如何在JavaScript单元测试中模拟本地存储?
有没有可以模拟本地存储的库 我一直在使用我的大多数其他javascript模拟,并发现它真的很棒 我的初步测试表明,localStorage拒绝在firefox(sadface)中分配,因此我可能需要一些技巧来解决这个问题:/ 我现在的选择(如我所见)如下:如何在JavaScript单元测试中模拟本地存储?,javascript,unit-testing,mocking,local-storage,sinon,Javascript,Unit Testing,Mocking,Local Storage,Sinon,有没有可以模拟本地存储的库 我一直在使用我的大多数其他javascript模拟,并发现它真的很棒 我的初步测试表明,localStorage拒绝在firefox(sadface)中分配,因此我可能需要一些技巧来解决这个问题:/ 我现在的选择(如我所见)如下: 创建我的所有代码都使用的包装函数并模拟它们 为localStorage创建某种状态管理(测试前的快照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
})();
我的初始测试表明localStorage拒绝在firefox中分配
只有在全球范围内。使用如上所述的包装器函数,它可以正常工作。这里有一个简单的方法,可以用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 = {};
});
});
如果要在所有测试中模拟本地存储,请在测试的全局范围内声明上面显示的
beforeach()
函数(通常是specHelper.js脚本)。不幸的是,在测试场景中模拟本地存储对象的唯一方法是更改我们正在测试的代码。您必须将代码包装在匿名函数中(无论如何都应该这样做),并使用“依赖项注入”来传递对窗口对象的引用。比如:
(function (window) {
// Your code
}(window.mockWindow || window));
然后,在测试内部,您可以指定:
window.mockWindow = { localStorage: { ... } };
还考虑在对象的构造函数中注入依赖项的选项。
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)
与模拟和单元测试一样,我喜欢避免测试存储实现。例如,在设置项目等之后,检查存储长度是否增加没有意义
由于替换真实localStorage对象上的方法显然不可靠,因此请使用“哑”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');
只需根据您的需要模拟全局本地存储/会话存储(它们具有相同的API)。
例如:
// 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;
}
};
}
然后你实际做的是这样的:
// mock the localStorage
window.localStorage = storageMock();
// mock the sessionStorage
window.sessionStorage = storageMock();
以下是使用sinon spy和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();
我决定重申我对彭彭80答案的评论,作为单独的答案,以便更容易将其作为一个库进行重用 我采用了Pumbaa80的代码,对其进行了一些改进,添加了测试,并将其发布为npm模块: 以下是源代码: 一些测试: 模块在全局对象(定义了窗口或全局对象)上创建模拟本地存储和会话存储 在我的另一个项目的测试中,我对mocha的要求如下:
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));
describe('someFunction', () => {
it('should remove some item from the local storage', () => {
const _localStorage = {
foo: 'bar', fizz: 'buzz'
}
Object.setPrototypeOf(_localStorage, {
removeItem: jest.fn()
})
jest.spyOn(global, 'localStorage', 'get').mockReturnValue(_localStorage)
someFunction()
expect(global.localStorage.removeItem).toHaveBeenCalledTimes(1)
expect(global.localStorage.removeItem).toHaveBeenCalledWith('whatever')
})
})
请注意,通过
Object.defineProperty
添加的所有方法都不会作为常规项进行迭代、访问或删除,也不会计入长度。此外,我还添加了一种注册回调的方法,当一个项即将放入对象时,会调用该方法。此回调可用于模拟测试中超出配额的错误。覆盖全局窗口对象的localStorage
属性(如某些答案中所建议的)在大多数JS引擎中不起作用,因为它们将localStorage
数据属性声明为不可写且不可配置
但是我发现,至少在PhantomJS(1.9.8版)的WebKit版本中,您可以使用遗留API\uuu defineGetter\uuuu
来控制访问本地存储时发生的情况。不过,如果这在其他浏览器中也能起作用,那就很有趣了
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 });
这种方法的好处是,您不必修改要测试的代码。这就是我要做的
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,
});
您不必将存储对象传递给使用它的每个方法。相反,您可以为任何接触存储适配器的模块使用配置参数
你的旧模块
// hard to test !
export const someFunction (x) {
window.localStorage.setItem('foo', x)
}
// hard to test !
export const anotherFunction () {
return window.localStorage.getItem('foo')
}
使用配置“包装器”函数创建新模块
当您在测试代码中使用模块时
// 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)
我喜欢这样做。保持简单
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;
});
});
});
当前的解决方案在Firefox中不起作用。这是因为html规范将localStorage定义为不可修改。但是,您可以通过直接访问localStorage的原型来解决这个问题
跨浏览器解决方案是模拟存储上的对象
使用
从bzbarsky和teogeos的回复中我发现我不需要嘲笑它。我可以通过setItem
将实际本地存储更改为我想要的状态,然后通过getItem
查询这些值,看看它是否已更改。它不像嘲弄那样强大,因为你看不出某件东西被改变了多少次,但它为我的目的发挥了作用。归功于
制作一个假的localstorage,并在localstorage被激活时监视它
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);
})
在这里我们使用它
it('providing search value should return matched item', () => {
localStorage.setItem('defaultLanguage', 'en-US');
expect(...
});
需要与存储的数据交互
很短的方法
const store = {};
Object.defineProperty(window, 'localStorage', {
value: {
getItem:(key) => store[key]},
setItem:(key, value) => {
store[key] = value.toString();
},
clear: () => {
store = {};
}
},
});
茉莉花间谍
如果你只是需要这些函数来监视他们使用茉莉花它会更简短,更容易阅读
Object.defineProperty(window, 'localStorage', {
value: {
getItem:(key) => {},
setItem:(key, value) => {},
clear: () => {},
...
},
});
const spy = spyOn(localStorage, 'getItem')
现在你根本不需要商店。我知道OP特别问到了模拟,但可以说,最好是监视
而不是模拟。如果您使用Object.keys(localStorage)
来迭代所有可用的键会怎么样?你
Object.defineProperty(window, 'localStorage', {
value: {
getItem:(key) => {},
setItem:(key, value) => {},
clear: () => {},
...
},
});
const spy = spyOn(localStorage, 'getItem')
const someFunction = () => {
const localStorageKeys = Object.keys(localStorage)
console.log('localStorageKeys', localStorageKeys)
localStorage.removeItem('whatever')
}
describe('someFunction', () => {
it('should remove some item from the local storage', () => {
const _localStorage = {
foo: 'bar', fizz: 'buzz'
}
Object.setPrototypeOf(_localStorage, {
removeItem: jest.fn()
})
jest.spyOn(global, 'localStorage', 'get').mockReturnValue(_localStorage)
someFunction()
expect(global.localStorage.removeItem).toHaveBeenCalledTimes(1)
expect(global.localStorage.removeItem).toHaveBeenCalledWith('whatever')
})
})