Im trying to write a Jest test case where code involved localstorage access.
While running test case is failing saying cant read property.
What is the way we need to write jest test case where localstorage.getItem() is involved
function myLocal(){
var value= localStorage.getItem('slot').time
return value
}
while running above sample code, Jest is throwing below error
TypeError: Cannot read property 'time' of null
Make your function testable:
function getTime(storage) {
var value = storage.getItem('slot').time
return value
}
Then use it in your application like that:
getTime(localStorage);
And test it like that:
const mockGetItem = jest.fn();
mockGetItem.mockReturnValue({time: '16:00'});
expect(getTime({getItem: mockGetItem})).toBe('16:00');
expect(mockGetItem).toHaveBeenCalledWith('slot');
You can mock it like anything else. This example is taken from some older version of jest-preset-angular i think:
const mock = () => {
let storage: { [key: string]: string } = {};
return {
getItem: (key: string) => (key in storage ? storage[key] : null),
setItem: (key: string, value: string) => (storage[key] = value || ''),
removeItem: (key: string) => delete storage[key],
clear: () => (storage = {})
};
};
Object.defineProperty(window, 'localStorage', { value: mock() });
Object.defineProperty(window, 'sessionStorage', { value: mock() });
No changes in your code. Of course you have to set the items you want to get first.
UPDATE:
I just noticed you are using plain JavaScript, not TypeScript. In that case you have to modify the mock:
const mock = function() {
const storage = {};
return {
getItem: (key) => (key in storage ? storage[key] : null),
setItem: (key, value) => (storage[key] = value || ''),
removeItem: (key) => delete storage[key],
clear: () => (storage = {})
};
};
Also, the example you posted in comments does not have a window object available, so you might have to mock that as well:
const window = {
localStorage: mock();
};
// or if you only use localStorage without window, this might suffice:
localStorage = mock();
How do I write a test that makes sure that the method reloadFn does in fact reload the window? I found this resource but I am unclear on how to expect a window reload when writing a test when that window reload happens in a given function. Thanks for the help!
const reloadFn = () => {
window.location.reload(true);
}
Updated Answer (November 2021)
Package:
"jest": "^26.6.0"
"#testing-library/jest-dom": "^5.11.4"
Build: create-react-app 4
describe("test window location's reload function", () => {
const original = window.location;
const reloadFn = () => {
window.location.reload(true);
};
beforeAll(() => {
Object.defineProperty(window, 'location', {
configurable: true,
value: { reload: jest.fn() },
});
});
afterAll(() => {
Object.defineProperty(window, 'location', { configurable: true, value: original });
});
it('mocks reload function', () => {
expect(jest.isMockFunction(window.location.reload)).toBe(true);
});
it('calls reload function', () => {
reloadFn(); // as defined above..
expect(window.location.reload).toHaveBeenCalled();
});
});
Note: Updated answer because the the old answer wasn't supported with latest jest version used in CRA.
Old answer
Here’s the solution but refactored for better organization:
describe('test window location\'s reload function', () => {
const { reload } = window.location;
beforeAll(() => {
Object.defineProperty(window.location, 'reload', {
configurable: true,
});
window.location.reload = jest.fn();
});
afterAll(() => {
window.location.reload = reload;
});
it('mocks reload function', () => {
expect(jest.isMockFunction(window.location.reload)).toBe(true);
});
it('calls reload function', () => {
reloadFn(); // as defined above..
expect(window.location.reload).toHaveBeenCalled();
});
});
Thanks :)
If you use TypeScript with Jest:
Idea
Create a copy and then delete window's location property.
Now set location property with reload function mocked.
Set the original value back when test completes.
Code: TypeScript 3.x and below
const location: Location = window.location;
delete window.location;
window.location = {
...location,
reload: jest.fn()
};
// <code to test>
// <code to test>
// <code to test>
expect(window.location.reload).toHaveBeenCalledTimes(1);
jest.restoreAllMocks();
window.location = location;
Code: TypeScript 4+
TypeScript 4 has stricter checks (which is a good thing), so I am not really sure if there's a way to do this other than to suppress the error using #ts-ignore or #ts-expect-error.
WARNING: Suppressing TypeScript validations can be 🔴dangerous🔴.
const location: Location = window.location;
// WARNING:
// #ts-ignore and #ts-expect-error suppress TypeScript validations by ignoring errors.
// Suppressing TypeScript validations can be 🔴dangerous🔴.
// #ts-ignore
delete window.location;
window.location = {
...location,
reload: jest.fn()
};
// <code to test>
// <code to test>
// <code to test>
expect(window.location.reload).toHaveBeenCalledTimes(1);
jest.restoreAllMocks();
window.location = location;
You can also simplify Murtaza Hussain's solution to
describe('refreshPage', () => {
const { reload } = window.location;
beforeAll(() => {
Object.defineProperty(window, 'location', {
writable: true,
value: { reload: jest.fn() },
});
});
afterAll(() => {
window.location.reload = reload;
});
it('reloads the window', () => {
refreshPage();
expect(window.location.reload).toHaveBeenCalled();
});
});
You could use sessionStorage to save a value for each reload.
As long as the browser does not close, the value will remain in sessionStorage.
When the page reloads the value will increment. Verify the fresh reload with this value.
Test this by pasting reloadFn() into the console.
The console will display Reload count: 1, and increment with each reload.
const reloadFn = () => {
window.location.reload(true);
}
window.onload = function() {
// get reloadCount from sessionStorage
reloadCount = sessionStorage.getItem('reloadCount');
// reloadCount will be null the first page load or a new value for each reload
if (reloadCount) {
// increment reloadCount
reloadCount = parseInt(reloadCount) + 1;
// save the new value to sessionStorage
sessionStorage.setItem('reloadCount', reloadCount);
console.log("Reload count: " + reloadCount);
} else {
// if reloadCount was null then set it to 1 and save to sessionStorage
sessionStorage.setItem('reloadCount', 1);
console.log("Page was loaded for the first time");
}
}
Inside your function testing reloadFn, you should use the mock code you linked to:
Object.defineProperty(window.location, 'reload', {
configurable: true,
}); // makes window.location.reload writable
window.location.reload = jest.fn(); // set up the mock
reloadFn(); // this should call your mock defined above
expect(window.location.reload).toHaveBeenCalled(); // assert the call
window.location.reload.mockRestore(); // restore window.location.reload to its original function
For a more improved test, you can use
expect(window.location.reload).toHaveBeenCalledWith(true);
Of note is that this is not actually verifying that the window is reloaded which is outside of the scope of a unit test. Something like browser testing or integration testing would verify that.
Instead of using workarounds with Object.defineProperty you can use native Jest's spyOn function in the following way:
test("reload test", () => {
const { getByText } = renderComponentWithReloadButton()
const reload = jest.fn()
jest
.spyOn(window, "location", "get")
.mockImplementation(() => ({ reload } as unknown as Location))
// Call an action that should trigger window.location.reload() function
act(() => {
getByText("Reload me").click()
})
// Test if `reload` function was really called
expect(reload).toBeCalled()
})
Also make sure you clear the mock after test by using for example jest.clearAllMocks() function.
If someone is looking this up in 2020, well i had the same issued.
Why is the problem happening for some and not for others?
Well it all depends on the version of chrome you are running, i wrote a test for a component that eventually called window.location.reload. Below is the section of the component code:
onConfirmChange() {
const {data, id} = this.state;
this.setState({showConfirmationModal: false}, () => {
this.update(data, id)
.then(() => window.location.reload());
});
}
The test initially failed on my build server with chrome version 71, it was passing for my local with chrome version 79.
I updated my chrome today to version 84, and the problem popped up in my local
The deleting of window.local seems to be un supported. Tried all the solution i could find in google nothing worked.
What is the solution then?
It was actually very simple, for react testing my system is using enzyme so what i did was wrapped the window.location.reload in an instance method and stubbed that in the test
JSX code:
reloadWindow() {
window.location.reload();
}
onConfirmChange() {
const {data, id} = this.state;
this.setState({showConfirmationModal: false}, () => {
this.update(data, id)
.then(() => reloadWindow());
});
}
TEST
it('check what happened', () => {
render();
const component = wrapper.instance();
sandbox.stub(component, 'reloadWindow').callsFake();
});
Updated Answer (May 2021)
I ran into some issues with a lot of the answers in the thread. I think version changes over time to the underlying libraries caused the breakage.
My Config:
"typescript": "~4.1.5"
"jest": "^26.6.3"
"jest-environment-jsdom": "^26.6.2"
Also, I should note, my solution is pretty verbose. But my use case needs to test window.location.replace() and the outcome. So I couldn't
simply mock window.location.replace. If you just need to mock one of the functions and don't care how the actual href changes, then some of the solutions in the thread will work great with less code.
Working Version
I found that completely polyfilling the window.location object solved all my problems.
window.location polyfill
Use this code and put it anywhere in your test file or setup files:
export class MockWindowLocation {
private _url: URL = new URL();
get href (): string {
return this._url.toString();
}
set href (url: string) {
this._url = new URL(url);
}
get protocol (): string {
return this._url.protocol;
}
get host (): string {
return this._url.host;
}
get hostname (): string {
return this._url.hostname;
}
get origin (): string {
return this._url.origin;
}
get port (): string {
return this._url.port;
}
get pathname (): string {
return this._url.pathname;
}
get hash (): string {
return this._url.hash;
}
get search (): string {
return this._url.search;
}
replace = jest.fn().mockImplementation((url: string) => {
this.href = url;
});
assign = jest.fn().mockImplementation((url: string) => {
this.href = url;
});
reload = jest.fn();
toString(): string {
return this._url.toString();
}
}
Test it
Then you have to delete window.location and set it to the new polyfill:
it('should be able to test window.location', () => {
delete window.location;
Object.defineProperty(window, 'location', {
value: new MockWindowLocation()
});
window.location.href = 'https://example.com/app/#/route/1';
window.location.reload();
expect(window.location.reload).toHaveBeenCalled();
expect(window.location.href).toBe('https://example.com/app/#/route/1');
expect(window.location.pathname).toBe('/app/');
expect(window.location.hash).toBe('#/route/1');
});
This works wonders for me. Hope it helps someone else out.
Other answers are simplier
To re-iterate, there are other answers in this thread that work just fine. I found:
Object.defineProperty(window, 'location', {
writable: true,
value: { reload: jest.fn() },
});
And:
const location: Location = window.location;
delete window.location;
window.location = {
...location,
reload: jest.fn()
};
both to be helpful. But like I said, I needed to spy on replace() and still have the standard functionality of window.location.
Hope this helps someone. Cheers!
I'm a very beginner with testing and started using jest. This function I have saves data in chrome.storage.local and I want to test if chrome.storage.local called 3 times. Here is the function:
saveData: function(){
chrome.storage.local.set({'blackList': bgModule.blackList}, function() {});
chrome.storage.local.set({'pastDays': bgModule.pastDays}, function() {});
chrome.storage.local.set({'websiteList': bgModule.websiteList}, function() {});
}
and here is my test:
const bgModule = require("../../app/Background/background.js");
const chrome = {
storage: {
local: {
set: function() {},
get: function() {}
}
}
};
let blackListStub = bgModule.blackList;
let pastDaysStub = [
{day: "day1"},
{day2: "day2"},
{day3: "day3"}];
let websiteListStub = [
{websiteName: "facebook.com"},
{websiteName: "stackoverflow.com"},
{websiteName: "github.com"}];
it ("should save data in local storage", () => {
spyOn(bgModule, 'saveData');
bgModule.saveData();
const spy = spyOn(chrome.storage.local, 'set');
spy({'blackList' : blackListStub});
spy({'pastDays' : pastDaysStub});
spy({'websiteList' : websiteListStub});
expect(spy).toHaveBeenCalledWith({'blackList' : blackListStub});
expect(spy).toHaveBeenCalledWith({'pastDays' : pastDaysStub});
expect(spy).toHaveBeenCalledWith({'websiteList' : websiteListStub});
expect(bgModule.saveData).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledTimes(3);
});
Test passes but it's incorrect. I want to make it work so when I change saveData it will break the test. The test I have has everything hardcoded into it and doesn't depend on saveData at all. I looked for example and tutorial materials and couldn't find anything for this case. Any help or direction I should take would be very helpful.
After help and correct answer with use of global.chrome refactored test looks like this:
it ("should save data in local storage", () => {
bgModule.saveData();
expect(chrome.storage.local.set).toHaveBeenCalledTimes(3);
});
The easiest thing would be to use dependency injection, so instead of calling chrome.storage.local directly, passed it as an argument to your function that then can be easily replaced by a mock in your test.
The other way would be to add it as a global variable into your test environment like this:
const get = jest.fn()
const set = jest.fn()
global.chrome = {
storage: {
local: {
set,
get
}
}
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.
Here is a simple way to mock it with Jasmine:
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 = {}));
});
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).
just mock the global localStorage / sessionStorage (they have the same API) for your needs.
For example:
// 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;
}
};
}
And then what you actually do, is something like that:
// mock the localStorage
window.localStorage = storageMock();
// mock the sessionStorage
window.sessionStorage = storageMock();
The current solutions will not work in Firefox. This is because localStorage is defined by the html spec as being not modifiable. You can however get around this by accessing localStorage's prototype directly.
The cross browser solution is to mock the objects on Storage.prototype e.g.
instead of spyOn(localStorage, 'setItem') use
spyOn(Storage.prototype, 'setItem')
spyOn(Storage.prototype, 'getItem')
taken from bzbarsky and teogeos's replies here https://github.com/jasmine/jasmine/issues/299
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');
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,
});
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.
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.
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)
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();
credits to
https://medium.com/#armno/til-mocking-localstorage-and-sessionstorage-in-angular-unit-tests-a765abdc9d87
Make a fake localstorage, and spy on localstorage, when it is caleld
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);
})
And here we use it
it('providing search value should return matched item', () => {
localStorage.setItem('defaultLanguage', 'en-US');
expect(...
});
I found that I did not need to mock it. I could change the actual local storage to the state I wanted it via setItem, then just query the values to see if it changed via getItem. It's not quite as powerful as mocking as you can't see how many times something was changed, but it worked for my purposes.
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.
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: { ... } };
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;
});
});
});
Need to interact with stored data
A quite short approach
const store = {};
Object.defineProperty(window, 'localStorage', {
value: {
getItem:(key) => store[key]},
setItem:(key, value) => {
store[key] = value.toString();
},
clear: () => {
store = {};
}
},
});
Spy with Jasmine
If you just need these functions to spy on them using jasmine it will be even shorter and easier to read.
Object.defineProperty(window, 'localStorage', {
value: {
getItem:(key) => {},
setItem:(key, value) => {},
clear: () => {},
...
},
});
const spy = spyOn(localStorage, 'getItem')
Now you don't need a store at all.
For those wanting to mock localstorage and not simply spy on it, this worked for me:
Storage.prototype.getItem = jest.fn(() => 'bla');
Source:
https://github.com/facebook/jest/issues/6858
I know OP specifically asked about mocking, but arguably it's better to spy rather than mock. And what if you use Object.keys(localStorage) to iterate over all available keys? You can test it like this:
const someFunction = () => {
const localStorageKeys = Object.keys(localStorage)
console.log('localStorageKeys', localStorageKeys)
localStorage.removeItem('whatever')
}
and the test code will be like follows:
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')
})
})
No need for mocks or constructors. Relatively few lines, too.
None of these answers are completely accurate or safe to use. Neither is this one but it is as accurate as I wanted without figuring out how to manipulate getters and setters.
TypeScript
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
}
};
Usage
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
);
});
});
Caveats
With localStorage you can do window.localStorage['color'] = 'red';
this will bypass the mock.
window.localStorage.length will bypass this mock.
window.localStorage.key throws in this mock as code relying on this can not be tested by this mock.
Mock correctly separates local and session storage.
Please also see: MDN: Web Storage API