Làm thế nào để giả lập localStorage trong các bài kiểm tra đơn vị JavaScript?


103

Có thư viện nào ngoài đó để chế nhạo không localStorage không?

Tôi đã sử dụng Sinon.JS cho hầu hết các thao tác chế giễu javascript khác của mình và tôi thấy nó thực sự tuyệt vời.

Thử nghiệm ban đầu của tôi cho thấy localStorage từ chối chuyển nhượng trong firefox (sadface), vì vậy có lẽ tôi sẽ cần một số loại hack xung quanh điều này: /

Các tùy chọn của tôi hiện tại (như tôi thấy) như sau:

  1. Tạo các hàm gói mà tất cả mã của tôi sử dụng và mô phỏng chúng
  2. Tạo một số loại quản lý trạng thái (có thể phức tạp) (snapshot localStorage trước khi kiểm tra, trong snapshot khôi phục dọn dẹp) cho localStorage.
  3. ??????

Bạn nghĩ gì về những cách tiếp cận này và bạn có nghĩ rằng có cách nào khác tốt hơn để thực hiện điều này không? Dù bằng cách nào, tôi sẽ đặt "thư viện" kết quả mà tôi đã tạo trên github cho nguồn mở tốt.


34
Bạn bỏ lỡ # 4:Profit!
Chris Laplante

Câu trả lời:


128

Đây là một cách đơn giản để chế nhạo nó với Jasmine:

beforeEach(function () {
  var store = {};

  spyOn(localStorage, 'getItem').andCallFake(function (key) {
    return store[key];
  });
  spyOn(localStorage, 'setItem').andCallFake(function (key, value) {
    return store[key] = value + '';
  });
  spyOn(localStorage, 'clear').andCallFake(function () {
      store = {};
  });
});

Nếu bạn muốn mô phỏng bộ lưu trữ cục bộ trong tất cả các thử nghiệm của mình, hãy khai báo beforeEach()hàm được hiển thị ở trên trong phạm vi chung của các thử nghiệm của bạn (vị trí thông thường là tập lệnh specHelper.js ).


1
+1 - bạn cũng có thể làm điều này với sinon. Điều quan trọng là tại sao bận tâm buộc để thử toàn bộ đối tượng localStorage, chỉ thử các phương pháp (GetItem và / hoặc SetItem) mà bạn đang quan tâm.
s1mm0t

6
Người đứng đầu lên: Dường như có một vấn đề với giải pháp này trong Firefox: github.com/pivotal/jasmine/issues/299
fwielstra

4
Tôi nhận được một ReferenceError: localStorage is not defined(đang chạy thử nghiệm bằng FB Jest và npm)… bất kỳ ý tưởng nào về cách giải quyết?
FeifanZ

1
Thử do thámwindow.localStorage
Benj

21
andCallFakethay đổi and.callFaketrong hoa nhài 2. +
Venugopal

51

chỉ cần giả lập localStorage / sessionStorage toàn cầu (chúng có cùng một API) cho nhu cầu của bạn.
Ví dụ:

 // Storage Mock
  function storageMock() {
    let storage = {};

    return {
      setItem: function(key, value) {
        storage[key] = value || '';
      },
      getItem: function(key) {
        return key in storage ? storage[key] : null;
      },
      removeItem: function(key) {
        delete storage[key];
      },
      get length() {
        return Object.keys(storage).length;
      },
      key: function(i) {
        const keys = Object.keys(storage);
        return keys[i] || null;
      }
    };
  }

Và sau đó những gì bạn thực sự làm, là một cái gì đó như thế:

// mock the localStorage
window.localStorage = storageMock();
// mock the sessionStorage
window.sessionStorage = storageMock();

1
Sửa gợi ý: getItemphải trả lại nullkhi giá trị không tồn tại: return storage[key] || null;;
cyberwombat

8
Kể từ năm 2016, có vẻ như điều này không hoạt động trong các trình duyệt hiện đại (đã kiểm tra Chrome và Firefox); ghi đè localStoragetổng thể là không thể.
jakub.g

2
Vâng, rất tiếc điều này không còn hoạt động nữa, nhưng tôi cũng cho rằng điều đó storage[key] || nulllà không chính xác. Nếu storage[key] === 0nó sẽ trở lại nullthay thế. Tôi nghĩ bạn có thể làm được return key in storage ? storage[key] : null.
redbmk

Chỉ cần sử dụng điều này trên SO! Công trình như một say mê - chỉ cần phải thay đổi localStor trở lại localStorage khi trên một máy chủ thựcfunction storageMock() { var storage = {}; return { setItem: function(key, value) { storage[key] = value || ''; }, getItem: function(key) { return key in storage ? storage[key] : null; }, removeItem: function(key) { delete storage[key]; }, get length() { return Object.keys(storage).length; }, key: function(i) { var keys = Object.keys(storage); return keys[i] || null; } }; } window.localStor = storageMock();
mplungjan

2
@ a8m Tôi gặp lỗi sau khi cập nhật nút lên 10.15.1 TypeError: Cannot set property localStorage of #<Window> which has only a getter, bất kỳ ý tưởng nào tôi có thể sửa lỗi này?
Tasawer Nawaz

19

Cũng xem xét tùy chọn để đưa các phụ thuộc vào hàm khởi tạo của đối tượng.

var SomeObject(storage) {
  this.storge = storage || window.localStorage;
  // ...
}

SomeObject.prototype.doSomeStorageRelatedStuff = function() {
  var myValue = this.storage.getItem('myKey');
  // ...
}

// In src
var myObj = new SomeObject();

// In test
var myObj = new SomeObject(mockStorage)

Cùng với việc chế nhạo và thử nghiệm đơn vị, tôi muốn tránh thử nghiệm việc triển khai bộ nhớ. Ví dụ: không cần kiểm tra xem thời lượng lưu trữ có tăng lên sau khi bạn đặt một mục hay không, v.v.

Vì rõ ràng là không đáng tin cậy để thay thế các phương thức trên đối tượng localStorage thực, hãy sử dụng một mockStorage "câm" và khai báo các phương thức riêng lẻ như mong muốn, chẳng hạn như:

var mockStorage = {
  setItem: function() {},
  removeItem: function() {},
  key: function() {},
  getItem: function() {},
  removeItem: function() {},
  length: 0
};

// Then in test that needs to know if and how setItem was called
sinon.stub(mockStorage, 'setItem');
var myObj = new SomeObject(mockStorage);

myObj.doSomeStorageRelatedStuff();
expect(mockStorage.setItem).toHaveBeenCalledWith('myKey');

1
Tôi nhận ra rằng đã lâu rồi tôi không xem câu hỏi này - nhưng thực tế đây là những gì tôi đã làm.
Anthony Sottile

1
Đây là giải pháp đáng giá duy nhất, vì nó không có nguy cơ cao bị hỏng trong thời gian.
oligofren

14

Đây là những gì tôi làm...

var mock = (function() {
  var store = {};
  return {
    getItem: function(key) {
      return store[key];
    },
    setItem: function(key, value) {
      store[key] = value.toString();
    },
    clear: function() {
      store = {};
    }
  };
})();

Object.defineProperty(window, 'localStorage', { 
  value: mock,
});

12

Các giải pháp hiện tại sẽ không hoạt động trong Firefox. Điều này là do localStorage được định nghĩa bởi thông số html là không thể sửa đổi. Tuy nhiên, bạn có thể giải quyết vấn đề này bằng cách truy cập trực tiếp vào nguyên mẫu của localStorage.

Giải pháp trình duyệt chéo là mô phỏng các đối tượng trên Storage.prototypeví dụ

thay vì spyOn (localStorage, 'SetItem') sử dụng

spyOn(Storage.prototype, 'setItem')
spyOn(Storage.prototype, 'getItem')

được lấy từ câu trả lời của bzbarskyteogeos tại đây https://github.com/jasmine/jasmine/issues/299


1
Bình luận của bạn sẽ nhận được nhiều lượt thích hơn. Cảm ơn bạn!
LorisBachert

6

Có thư viện nào ngoài đó để chế nhạolocalStorage không?

Tôi chỉ viết một:

(function () {
    var localStorage = {};
    localStorage.setItem = function (key, val) {
         this[key] = val + '';
    }
    localStorage.getItem = function (key) {
        return this[key];
    }
    Object.defineProperty(localStorage, 'length', {
        get: function () { return Object.keys(this).length - 2; }
    });

    // Your tests here

})();

Thử nghiệm ban đầu của tôi cho thấy localStorage từ chối gán trong firefox

Chỉ trong bối cảnh toàn cầu. Với một hàm wrapper như trên, nó hoạt động tốt.


1
bạn cũng có thể sử dụngvar window = { localStorage: ... }
user123444555621

1
Thật không may, điều đó có nghĩa là tôi sẽ cần biết mọi thuộc tính tôi cần và đã thêm vào đối tượng window (và tôi bỏ lỡ nguyên mẫu của nó, v.v.). Bao gồm bất kỳ jQuery nào có thể cần. Thật không may, điều này có vẻ như là một giải pháp không. Ngoài ra, các bài kiểm tra là mã kiểm tra sử dụng localStorage, các bài kiểm tra không nhất thiết phải có localStoragetrực tiếp trong đó. Giải pháp này không thay đổi localStoragecác tập lệnh khác nên nó không phải là giải pháp. +1 cho thủ thuật xác định phạm vi mặc dù
Anthony Sottile

1
Bạn có thể cần điều chỉnh mã của mình để làm cho nó có thể kiểm tra được. Tôi biết điều này rất khó chịu và đó là lý do tại sao tôi thích thử nghiệm selen nặng hơn các bài kiểm tra đơn vị.
user123444555621

Đây không phải là một giải pháp hợp lệ. Nếu bạn gọi bất kỳ hàm nào từ bên trong hàm ẩn danh đó, bạn sẽ mất tham chiếu đến cửa sổ mô phỏng hoặc đối tượng localStorage giả. Mục đích của bài kiểm tra đơn vị là bạn phải gọi một hàm bên ngoài. Vì vậy, khi bạn gọi hàm hoạt động với localStorage, nó sẽ không sử dụng mô hình. Thay vào đó, bạn phải bọc mã bạn đang thử nghiệm trong một chức năng ẩn danh. Để làm cho nó có thể kiểm tra được, hãy để nó chấp nhận đối tượng window làm tham số.
John Kurlak

Mô hình đó có một lỗi: Khi truy xuất một mục không tồn tại, getItem sẽ trả về null. Trong mô hình, nó trả về không xác định. Mã đúng phải làif this.hasOwnProperty(key) return this[key] else return null
Evan

4

Đây là một ví dụ sử dụng sinon spy và mock:

// window.localStorage.setItem
var spy = sinon.spy(window.localStorage, "setItem");

// You can use this in your assertions
spy.calledWith(aKey, aValue)

// Reset localStorage.setItem method    
spy.reset();



// window.localStorage.getItem
var stub = sinon.stub(window.localStorage, "getItem");
stub.returns(aValue);

// You can use this in your assertions
stub.calledWith(aKey)

// Reset localStorage.getItem method
stub.reset();

4

Việc ghi đè thuộc localStoragetính của windowđối tượng toàn cục như được đề xuất trong một số câu trả lời sẽ không hoạt động trong hầu hết các công cụ JS, bởi vì chúng khai báolocalStorage dữ liệu là không thể ghi và không thể định cấu hình.

Tuy nhiên, tôi phát hiện ra rằng ít nhất với phiên bản WebKit của PhantomJS (phiên bản 1.9.8), bạn có thể sử dụng API kế thừa __defineGetter__để kiểm soát những gì xảy ra nếu localStorageđược truy cập. Tuy nhiên, sẽ rất thú vị nếu điều này cũng hoạt động trên các trình duyệt khác.

var tmpStorage = window.localStorage;

// replace local storage
window.__defineGetter__('localStorage', function () {
    throw new Error("localStorage not available");
    // you could also return some other object here as a mock
});

// do your tests here    

// restore old getter to actual local storage
window.__defineGetter__('localStorage',
                        function () { return tmpStorage });

Lợi ích của phương pháp này là bạn sẽ không phải sửa đổi mã mà bạn sắp kiểm tra.


Chỉ cần lưu ý rằng điều này sẽ không hoạt động trong PhantomJS 2.1.1. ;)
Conrad Calmez

4

Bạn không cần phải chuyển đối tượng lưu trữ cho mỗi phương thức sử dụng nó. Thay vào đó, bạn có thể sử dụng thông số cấu hình cho bất kỳ mô-đun nào chạm vào bộ điều hợp lưu trữ.

Mô-đun cũ của bạn

// hard to test !
export const someFunction (x) {
  window.localStorage.setItem('foo', x)
}

// hard to test !
export const anotherFunction () {
  return window.localStorage.getItem('foo')
}

Mô-đun mới của bạn với chức năng "wrapper" cấu hình

export default function (storage) {
  return {
    someFunction (x) {
      storage.setItem('foo', x)
    }
    anotherFunction () {
      storage.getItem('foo')
    }
  }
}

Khi bạn sử dụng mô-đun trong mã thử nghiệm

// import mock storage adapater
const MockStorage = require('./mock-storage')

// create a new mock storage instance
const mock = new MockStorage()

// pass mock storage instance as configuration argument to your module
const myModule = require('./my-module')(mock)

// reset before each test
beforeEach(function() {
  mock.clear()
})

// your tests
it('should set foo', function() {
  myModule.someFunction('bar')
  assert.equal(mock.getItem('foo'), 'bar')
})

it('should get foo', function() {
  mock.setItem('foo', 'bar')
  assert.equal(myModule.anotherFunction(), 'bar')
})

Các MockStoragelớp có thể trông như thế này

export default class MockStorage {
  constructor () {
    this.storage = new Map()
  }
  setItem (key, value) {
    this.storage.set(key, value)
  }
  getItem (key) {
    return this.storage.get(key)
  }
  removeItem (key) {
    this.storage.delete(key)
  }
  clear () {
    this.constructor()
  }
}

Khi sử dụng mô-đun của bạn trong mã sản xuất, thay vào đó hãy chuyển bộ điều hợp localStorage thực

const myModule = require('./my-module')(window.localStorage)

fyi đối với mọi người, điều này chỉ hợp lệ trong es6: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… (nhưng là một giải pháp tuyệt vời và tôi không thể đợi cho đến khi nó có sẵn ở mọi nơi!)
Alex Moore- Niemi

@ AlexMoore-Niemi có rất ít việc sử dụng ES6 ở đây. Tất cả có thể được thực hiện bằng ES5 hoặc thấp hơn với rất ít thay đổi.
Cảm ơn bạn

vâng, chỉ cần chỉ ra export default functionvà khởi tạo một mô-đun với một đối số như vậy mà thôi. mô hình đứng bất chấp.
Alex Moore-Niemi

Huh? Tôi đã phải sử dụng kiểu cũ hơn requiređể nhập một mô-đun và áp dụng nó cho một đối số trong cùng một biểu thức. Không có cách nào để làm điều đó trong ES6 mà tôi biết. Nếu không, tôi đã sử dụng ES6import
Cảm ơn bạn

2

Tôi quyết định nhắc lại nhận xét của mình cho câu trả lời của Pumbaa80 dưới dạng câu trả lời riêng biệt để việc sử dụng lại nó làm thư viện dễ dàng hơn.

Tôi đã lấy mã của Pumbaa80, tinh chỉnh một chút, thêm các bài kiểm tra và xuất bản nó dưới dạng mô-đun npm tại đây: https://www.npmjs.com/package/mock-local-storage .

Đây là một mã nguồn: https://github.com/letsrock-today/mock-local-storage/blob/master/src/mock-localstorage.js

Một số bài kiểm tra: https://github.com/letsrock-today/mock-local-storage/blob/master/test/mock-localstorage.js

Mô-đun tạo localStorage giả và sessionStorage trên đối tượng toàn cục (cửa sổ hoặc toàn cục, đối tượng nào trong số chúng được xác định).

Trong các thử nghiệm dự án khác của tôi, tôi yêu cầu nó với mocha như sau: mocha -r mock-local-storageđể cung cấp các định nghĩa toàn cầu cho tất cả các mã đang thử nghiệm.

Về cơ bản, mã trông như sau:

(function (glob) {

    function createStorage() {
        let s = {},
            noopCallback = () => {},
            _itemInsertionCallback = noopCallback;

        Object.defineProperty(s, 'setItem', {
            get: () => {
                return (k, v) => {
                    k = k + '';
                    _itemInsertionCallback(s.length);
                    s[k] = v + '';
                };
            }
        });
        Object.defineProperty(s, 'getItem', {
            // ...
        });
        Object.defineProperty(s, 'removeItem', {
            // ...
        });
        Object.defineProperty(s, 'clear', {
            // ...
        });
        Object.defineProperty(s, 'length', {
            get: () => {
                return Object.keys(s).length;
            }
        });
        Object.defineProperty(s, "key", {
            // ...
        });
        Object.defineProperty(s, 'itemInsertionCallback', {
            get: () => {
                return _itemInsertionCallback;
            },
            set: v => {
                if (!v || typeof v != 'function') {
                    v = noopCallback;
                }
                _itemInsertionCallback = v;
            }
        });
        return s;
    }

    glob.localStorage = createStorage();
    glob.sessionStorage = createStorage();
}(typeof window !== 'undefined' ? window : global));

Lưu ý rằng tất cả các phương thức được thêm qua Object.definePropertyđể chúng không bị lặp lại, bị truy cập hoặc bị xóa như các mục thông thường và sẽ không được tính về độ dài. Ngoài ra, tôi đã thêm một cách để đăng ký gọi lại được gọi khi một mục sắp được đưa vào đối tượng. Lệnh gọi lại này có thể được sử dụng để mô phỏng lỗi vượt quá hạn ngạch trong các thử nghiệm.


2

Tôi thấy rằng tôi không cần phải chế nhạo nó. Tôi có thể thay đổi bộ nhớ cục bộ thực tế thành trạng thái mà tôi muốn setItem, sau đó chỉ cần truy vấn các giá trị để xem liệu nó có thay đổi hay không getItem. Nó không hoàn toàn mạnh mẽ như chế nhạo vì bạn không thể thấy thứ gì đó đã được thay đổi bao nhiêu lần, nhưng nó hoạt động cho mục đích của tôi.


0

Thật không may, cách duy nhất chúng ta có thể giả lập đối tượng localStorage trong một kịch bản thử nghiệm là thay đổi mã mà chúng ta đang thử nghiệm. Bạn phải bọc mã của mình trong một chức năng ẩn danh (mà bạn vẫn nên làm) và sử dụng "phụ thuộc chèn" để chuyển một tham chiếu đến đối tượng window. Cái gì đó như:

(function (window) {
   // Your code
}(window.mockWindow || window));

Sau đó, bên trong thử nghiệm của mình, bạn có thể chỉ định:

window.mockWindow = { localStorage: { ... } };

0

Đây là cách tôi thích làm điều đó. Giữ nó đơn giản.

  let localStoreMock: any = {};

  beforeEach(() => {

    angular.mock.module('yourApp');

    angular.mock.module(function ($provide: any) {

      $provide.service('localStorageService', function () {
        this.get = (key: any) => localStoreMock[key];
        this.set = (key: any, value: any) => localStoreMock[key] = value;
      });

    });
  });

0

ghi có vào https://medium.com/@armno/til-mocking-localstorage-and-sessionstorage-in-angular-unit-tests-a765abdc9d87 Tạo cục bộ cục bộ giả và theo dõi cục bộ lưu trữ, khi nó được caleld

 beforeAll( () => {
    let store = {};
    const mockLocalStorage = {
      getItem: (key: string): string => {
        return key in store ? store[key] : null;
      },
      setItem: (key: string, value: string) => {
        store[key] = `${value}`;
      },
      removeItem: (key: string) => {
        delete store[key];
      },
      clear: () => {
        store = {};
      }
    };

    spyOn(localStorage, 'getItem')
      .and.callFake(mockLocalStorage.getItem);
    spyOn(localStorage, 'setItem')
      .and.callFake(mockLocalStorage.setItem);
    spyOn(localStorage, 'removeItem')
      .and.callFake(mockLocalStorage.removeItem);
    spyOn(localStorage, 'clear')
      .and.callFake(mockLocalStorage.clear);
  })

Và ở đây chúng tôi sử dụng nó

it('providing search value should return matched item', () => {
    localStorage.setItem('defaultLanguage', 'en-US');

    expect(...
  });
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.