如何在JavaScriptunit testing中模拟localStorage?
有没有图书馆来嘲笑localStorage
?
我一直在使用Sinon.JS的大部分我的其他JavaScript嘲笑,发现它真的很棒。
我最初的testing表明,localStorage拒绝在Firefox(sadface)中分配,所以我可能需要一些破解这个:/
我现在的select(如我所见)如下:
- 创build所有我的代码使用的包装function,并嘲笑这些
- 为localStorage创build某种(可能是复杂的)状态pipe理(testing之前的快照localStorage,在清理恢复快照中)。
-
??????
你如何看待这些方法?你认为还有其他更好的方法可以解决吗? 无论哪种方式,我将把最终产生的“库”放在github上,以获得开源的好处。
这是一个简单的方法来嘲笑它与茉莉花:
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 = {}; }); });
如果要在所有testing中模拟本地存储,请在testing的全局范围内声明beforeEach()
函数(通常的地方是specHelper.js脚本)。
只是嘲笑全球localStorage / sessionStorage(他们有相同的API)为您的需要。
例如:
// 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; } }; }
然后你真正做的是这样的:
// mock the localStorage window.localStorage = storageMock(); // mock the sessionStorage window.sessionStorage = storageMock();
还要考虑在对象的构造函数中注入依赖项的选项。
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)
根据模拟和unit testing,我喜欢避免testing存储实现。 例如,在设置一个项目之后,检查存储的长度是否增加没有意义。
由于在真正的localStorage对象上replace方法显然是不可靠的,所以使用“dumb”mockStorage并根据需要存储各个方法,比如:
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');
有没有图书馆来嘲笑
localStorage
?
我只写了一个:
(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 })();
我最初的testing显示localStorage拒绝在Firefox中分配
只在全球范围内。 使用上面的包装函数,它工作得很好。
这是我做的…
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, });
这是一个使用sinon间谍和模拟的例子:
// 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();
您不必将存储对象传递给每个使用它的方法。 相反,您可以为接触存储适配器的任何模块使用configuration参数。
你的旧模块
// hard to test ! export const someFunction (x) { window.localStorage.setItem('foo', x) } // hard to test ! export const anotherFunction () { return window.localStorage.getItem('foo') }
你的新模块configuration“包装”function
export default function (storage) { return { someFunction (x) { storage.setItem('foo', x) } anotherFunction () { storage.getItem('foo') } } }
在testing代码中使用模块时
// 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') })
MockStorage
类可能看起来像这样
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() } }
在生产代码中使用模块时,请传递实际的localStorage适配器
const myModule = require('./my-module')(window.localStorage)
按照某些答案中的build议覆盖全局window
对象的localStorage
属性在大多数JS引擎中不起作用,因为它们将localStorage
数据属性声明为不可写和不可configuration。
不过我发现,至less在PhantomJS(版本1.9.8)WebKit版本中,您可以使用传统API __defineGetter__
来控制在访问localStorage
时会发生什么情况。 如果这在其他浏览器中也是有用的,那也是很有意思的。
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 });
这种方法的好处是你不需要修改你要testing的代码。
不幸的是,我们可以在testing场景中模拟localStorage对象的唯一方法是更改我们正在testing的代码。 你必须把你的代码包装在一个匿名函数中(你应该这样做),并使用“dependency injection”来传递一个对象的引用。 就像是:
(function (window) { // Your code }(window.mockWindow || window));
然后,在你的testing中,你可以指定:
window.mockWindow = { localStorage: { ... } };
我决定重申我对Pumbaa80答案的评论作为单独的答案,以便将其重用为一个库。
我拿了Pumbaa80的代码,细化了一下,添加了testing,并将其作为npm模块发布在这里: https ://www.npmjs.com/package/mock-local-storage。
这里是一个源代码: https : //github.com/letsrock-today/mock-local-storage/blob/master/src/mock-localstorage.js
一些testing: https : //github.com/letsrock-today/mock-local-storage/blob/master/test/mock-localstorage.js
模块在全局对象(窗口或全局,其中哪些被定义)上创build模拟localStorage和sessionStorage。
在我的另一个项目的testing中,我用摩卡要求这样做: mocha -r mock-local-storage
使全局定义适用于所有待测代码。
基本上,代码如下所示:
(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));
请注意,通过Object.defineProperty
添加的所有方法都不会像常规项目那样迭代,访问或删除,也不会计入长度。 另外我添加了一个方法来注册callback,当一个项目即将被放入对象时被调用。 此callback可用于模拟testing中的配额超出错误。