可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
Are there any libraries out there to mock localStorage
?
I've been using Sinon.JS for most of my other javascript mocking and have found it is really great.
My initial testing shows that localStorage refuses to be assignable in firefox (sadface) so I'll probably need some sort of hack around this :/
My options as of now (as I see) are as follows:
- Create wrapping functions that all my code uses and mock those
- Create some sort of (might be complicated) state management (snapshot localStorage before test, in cleanup restore snapshot) for localStorage.
??????
What do you think of these approaches and do you think there are any other better ways to go about this? Either way I'll put the resulting "library" that I end up making on github for open source goodness.
回答1:
Here is a simple way to mock it with 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 = {}; }); });
If you want to mock the local storage in all your tests, declare the beforeEach()
function shown above in the global scope of your tests (the usual place is a specHelper.js script).
回答2:
just mock the global localStorage / sessionStorage (they have the same API) for your needs.
For example:
// Storage Mock function 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; } }; }
And then what you actually do, is something like that:
// mock the localStorage window.localStorage = storageMock(); // mock the sessionStorage window.sessionStorage = storageMock();
回答3:
Also consider the option to inject dependencies in an object's constructor function.
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)
In line with mocking and unit testing, I like to avoid testing the storage implementation. For instance no point in checking if length of storage increased after you set an item, etc.
Since it is obviously unreliable to replace methods on the real localStorage object, use a "dumb" mockStorage and stub the individual methods as desired, such as:
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');
回答4:
Are there any libraries out there to mock localStorage
?
I just wrote one:
(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 })();
My initial testing shows that localStorage refuses to be assignable in firefox
Only in global context. With a wrapper function as above, it works just fine.
回答5:
This is what I do...
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, });
回答6:
Here is an exemple using sinon spy and 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();
回答7:
You don't have to pass the storage object to each method that uses it. Instead, you can use a configuration parameter for any module that touches the storage adapter.
Your old module
// hard to test ! export const someFunction (x) { window.localStorage.setItem('foo', x) } // hard to test ! export const anotherFunction () { return window.localStorage.getItem('foo') }
Your new module with config "wrapper" function
export default function (storage) { return { someFunction (x) { storage.setItem('foo', x) } anotherFunction () { storage.getItem('foo') } } }
When you use the module in testing code
// 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') })
The MockStorage
class might look like this
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() } }
When using your module in production code, instead pass the real localStorage adapter
const myModule = require('./my-module')(window.localStorage)
回答8:
Overwriting the localStorage
property of the global window
object as suggested in some of the answers won't work in most JS engines, because they declare the localStorage
data property as not writable and not configurable.
However I found out that at least with PhantomJS's (version 1.9.8) WebKit version you could use the legacy API __defineGetter__
to control what happens if localStorage
is accessed. Still it would be interesting if this works in other browsers as well.
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 });
The benefit of this approach is that you would not have to modify the code you're about to test.
回答9:
Unfortunately, the only way we can mock the localStorage object in a test scenario is to change the code we're testing. You have to wrap your code in an anonymous function (which you should be doing anyway) and use "dependency injection" to pass in a reference to the window object. Something like:
(function (window) { // Your code }(window.mockWindow || window));
Then, inside of your test, you can specify:
window.mockWindow = { localStorage: { ... } };
回答10:
I decided to reiterate my comment to Pumbaa80's answer as separate answer so that it'll be easier to reuse it as a library.
I took Pumbaa80's code, refined it a bit, added tests and published it as an npm module here: https://www.npmjs.com/package/mock-local-storage.
Here is a source code: https://github.com/letsrock-today/mock-local-storage/blob/master/src/mock-localstorage.js
Some tests: https://github.com/letsrock-today/mock-local-storage/blob/master/test/mock-localstorage.js
Module creates mock localStorage and sessionStorage on the global object (window or global, which of them is defined).
In my other project's tests I required it with mocha as this: mocha -r mock-local-storage
to make global definitions available for all code under test.
Basically, code looks like follows:
(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));
Note that all methods added via Object.defineProperty
so that them won't be iterated, accessed or removed as regular items and won't count in length. Also I added a way to register callback which is called when an item is about to be put into object. This callback may be used to emulate quota exceeded error in tests.
回答11:
This is how I like to do it. Keeps it simple.
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; }); }); });