Как насмехаться с localStorage в модульных тестах JavaScript?
Есть ли там какие-нибудь библиотеки, чтобы издеваться над localStorage
?
Я использовал Sinon.JS для большей части моего другого javascript, насмехающегося, и нашел, что это действительно большой.
Мое начальное тестирование показывает, что localStorage отказывается присваиваться в firefox (sadface), поэтому мне, вероятно, понадобится какой-то хак по этому поводу:/
Мои параметры на данный момент (как я вижу) следующие:
- Создайте функции обертывания, которые использует весь мой код и издеваются над этими
- Создайте какое-то (может быть сложное) управление состоянием (моментальный снимок localStorage перед тестом, в моментальном снимке восстановления очистки) для localStorage.
-
??????
Что вы думаете об этих подходах, и думаете ли вы, что есть другие способы улучшить это? В любом случае я положу полученную в результате "библиотеку", которую я в конечном итоге сделаю на github для доброты с открытым исходным кодом.
Ответы
Ответ 1
Вот простой способ издеваться над 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 = {};
});
});
Если вы хотите издеваться над локальным хранилищем во всех своих тестах, объявите функцию beforeEach()
, показанную выше в глобальной области ваших тестов (обычным местом является specHelper.js script).
Ответ 2
просто издеваются над глобальным локальным хранилищем /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();
Ответ 3
Также рассмотрим возможность встраивания зависимостей в функцию конструктора объектов.
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');
Ответ 4
Это то, что я делаю...
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,
});
Ответ 5
Есть ли там какие-нибудь библиотеки, чтобы издеваться над 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
Только в глобальном контексте. С помощью функции обертки, как указано выше, она работает нормально.
Ответ 6
Вот пример использования 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();
Ответ 7
Перезапись свойства localStorage
глобального объекта window
, как предложено в некоторых ответах, не будет работать в большинстве движков JS, потому что они объявляют свойство данных localStorage
как незаписываемое и не настраиваемое.
Однако я узнал, что, по крайней мере, с версией WebKit от PhantomJS (версия 1.9.8) вы можете использовать устаревший 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 });
Преимущество такого подхода заключается в том, что вам не придется изменять код, который вы собираетесь тестировать.
Ответ 8
Вам не нужно передавать объект хранения каждому методу, который его использует. Вместо этого вы можете использовать параметр конфигурации для любого модуля, который касается адаптера хранения.
Ваш старый модуль
// hard to test !
export const someFunction (x) {
window.localStorage.setItem('foo', x)
}
// hard to test !
export const anotherFunction () {
return window.localStorage.getItem('foo')
}
Ваш новый модуль с функцией "обертка" config
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)
Ответ 9
Текущие решения не будут работать в Firefox. Это потому, что localStorage определяется спецификацией html как не подлежащий изменению. Однако вы можете обойти это, непосредственно обращаясь к прототипу localStorage.
Кросс-браузерное решение состоит в том, чтобы макетировать объекты в Storage.prototype
например
вместо spyOn (localStorage, 'setItem') используйте
spyOn(Storage.prototype, 'setItem')
spyOn(Storage.prototype, 'getItem')
взяты из ответов bzbarsky и teogeos здесь https://github.com/jasmine/jasmine/issues/299
Ответ 10
Я решил повторить свой комментарий к ответу 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
Модуль создает mock localStorage и sessionStorage для глобального объекта (окна или глобального, какой из них определен).
В моих других проектных тестах я требовал его с помощью 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));
Обратите внимание, что все методы, добавленные через Object.defineProperty
, чтобы они не были итерированы, доступны или удалены как обычные элементы и не будут учитываться в длине. Также я добавил способ зарегистрировать обратный вызов, который вызывается, когда предмет собирается быть помещен в объект. Этот обратный вызов может использоваться для эмуляции превышения квоты в тестах.
Ответ 11
К сожалению, единственный способ обмануть объект localStorage в тестовом сценарии - изменить код, который мы тестируем. Вы должны обернуть свой код анонимной функцией (которую вы все равно должны делать) и использовать "инъекцию зависимостей" для передачи в ссылку на объект окна. Что-то вроде:
(function (window) {
// Your code
}(window.mockWindow || window));
Затем внутри вашего теста вы можете указать:
window.mockWindow = { localStorage: { ... } };
Ответ 12
Вот как я люблю это делать. Это просто.
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;
});
});
});
Ответ 13
Я обнаружил, что мне не нужно это издеваться. Я мог бы изменить фактическое локальное хранилище на требуемое состояние через setItem
, а затем просто запросить значения, чтобы увидеть, изменилось ли оно через getItem
. Это не так сильно, как насмешка, как вы не можете видеть, сколько раз что-то менялось, но это работало для моих целей.