如何在 JavaScript 单元测试中模拟 localStorage?

有没有模拟 localStorage的库?

我一直在使用 Sinon JS的大部分其他 javascript 嘲笑,并已发现它真的是伟大的。

我最初的测试表明 localStorage 拒绝在 firefox (sadface)中被分配,所以我可能需要一些解决方案:/

我目前的选择如下:

  1. 创建所有代码都使用的包装函数,并对其进行模拟
  2. 为 localStorage 创建某种(可能比较复杂的)状态管理(测试前的快照 localStorage,在清理还原快照中)。
  3. ??????

你对这些方法有什么看法? 你认为还有其他更好的方法吗?无论哪种方式,我都会把最终产生的“库”放在 github 上,以获得开源的好处。

121987 次浏览

有没有模拟 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 中被分配

只有在全球范围内,有了上述包装函式,它才能正常工作。

这里有一个简单的方法来嘲笑它与茉莉:

let localStore;


beforeEach(() => {
localStore = {};


spyOn(window.localStorage, 'getItem').and.callFake((key) =>
key in localStore ? localStore[key] : null
);
spyOn(window.localStorage, 'setItem').and.callFake(
(key, value) => (localStore[key] = value + '')
);
spyOn(window.localStorage, 'clear').and.callFake(() => (localStore = {}));
});

如果希望在所有测试中模拟本地存储,那么在测试的全局范围中声明上面显示的 beforeEach()函数(通常是使用 SpecHelper.js脚本)。

不幸的是,在测试场景中模拟 localStorage 对象的唯一方法是更改我们正在测试的代码。你必须将你的代码封装在一个匿名函数中(无论如何你都应该这么做) ,然后使用“依赖注入”来传递对 window 对象的引用。比如:

(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 对象上的方法显然是不可靠的,所以使用“哑”模拟存储并根据需要存根单个方法,例如:

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/sessionStorage (它们具有相同的 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 谍和 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();

我决定重申我对 Pumbaa80答案的评论,作为单独的答案,以便更容易地将其重用为库。

我使用了 Pumbaa80的代码,对它进行了一些改进,添加了测试,并在这里将它作为一个 npm 模块发布: Https://www.npmjs.com/package/mock-local-storage.

下面是源代码: Https://github.com/letsrock-today/mock-local-storage/blob/master/src/mock-localstorage.js

一些测试: Https://github.com/letsrock-today/mock-local-storage/blob/master/test/mock-localstorage.js

模块在全局对象上创建模拟 localStorage 和 sessionStorage (窗口或全局,它们中的哪一个是定义的)。

在我的其他项目的测试中,我要求它与摩卡如下: 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添加的方法都不会作为常规项进行迭代、访问或删除,并且不会计算长度。此外,我添加了一种方法来注册回调,这是调用时,一个项目是要放入对象。此回调可用于模拟测试中超出配额的错误。

按照一些答案中的建议,覆盖全局 window对象的 localStorage属性在大多数 JS 引擎中不会起作用,因为它们将 localStorage数据属性声明为不可写和不可配置。

然而,我发现至少在 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 });

这种方法的好处是您不必修改要测试的代码。

这就是我的工作。

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')
}

具有配置“ wrapper”函数的新模块

export default function (storage) {
return {
someFunction (x) {
storage.setItem('foo', x)
}
anotherFunction () {
storage.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 中使用。这是因为 localStorage 是由 html 规范定义为不可修改的。然而,您可以通过直接访问 localStorage 的原型来绕过这个问题。

跨浏览器解决方案是模拟 Storage.prototype上的对象,例如。

而不是使用 spyOn (localStorage,‘ setItem’)

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

摘自 BzbarskyTeogeos的答复在这里 https://github.com/jasmine/jasmine/issues/299

我发现我不需要嘲笑它。我可以通过 setItem将实际的本地存储更改为我想要的状态,然后查询这些值,看看它是否通过 getItem更改。它没有嘲笑那么强大,因为你看不到有多少次,某些东西被改变了,但它为我的目的工作。

归功于 Https://medium.com/@armno/til-mocking-localstorage-and-sessionstorage-in-angular-unit-tests-a765abdc9d87 创建一个假的本地存储,并在本地存储被调用时监视它

 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 特别要求关于嘲笑,但可以说它是更好的 spy而不是 mock。如果使用 Object.keys(localStorage)迭代所有可用的键会怎样?你可以这样测试:

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')
})
})

不需要模拟或构造函数,而且代码行也相对较少。

这些答案没有一个是完全准确或者可以安全使用的。这个也不是,但是它和我想要的一样精确,而没有弄清楚如何操作 getter 和 setter。

打字机

const mockStorage = () => {
for (const storage of [window.localStorage, window.sessionStorage]) {
let store = {};


spyOn(storage, 'getItem').and.callFake((key) =>
key in store ? store[key] : null
);
spyOn(storage, 'setItem').and.callFake(
(key, value) => (store[key] = value + '')
);
spyOn(storage, 'removeItem').and.callFake((key: string) => {
delete store[key];
});
spyOn(storage, 'clear').and.callFake(() => (store = {}));
spyOn(storage, 'key').and.callFake((i: number) => {
throw new Error(`Method 'key' not implemented`);
});
// Storage.length is not supported
// Property accessors are not supported
}
};

用法

describe('Local storage', () => {
beforeEach(() => {
mockStorage();
});


it('should cache a unit in session', () => {
LocalStorageService.cacheUnit(testUnit);
expect(window.sessionStorage.setItem).toHaveBeenCalledTimes(1);
expect(window.sessionStorage.getItem(StorageKeys.units)).toContain(
testUnit.id
);
});
});

警告

  • 使用 localStorage,您可以执行 window.localStorage['color'] = 'red'; 这会绕过模拟。
  • window.localStorage.length将绕过这个模拟。
  • window.localStorage.key引入了这个 mock,因为依赖于它的代码无法通过这个 mock 进行测试。
  • Mock 正确地分隔了本地存储和会话存储。

请参阅: Web 存储 API