How to augment instances of a mocked constructor in Jest - javascript

I'd like to augment, but not completely replace, instances of a mocked constructor in a Jest unit test.
I want to add a few values to the instance, but keep the auto-mocked goodness of Jest.
For example:
A.js
module.exports = class A {
constructor(value) {
this.value = value;
}
getValue() {
return this.value;
}
}
To get some auto-mock awesomeness:
jest.mock('./A');
With the automock, instances have a mocked .getValue() method, but they do not have the .value property.
A documented way of mocking constructors is:
// SomeClass.js
module.exports = class SomeClass {
m(a, b) {}
}
// OtherModule.test.js
jest.mock('./SomeClass'); // this happens automatically with automocking
const SomeClass = require('./SomeClass')
const mMock = jest.fn()
SomeClass.mockImplementation(() => {
return {
m: mMock
}
})
const some = new SomeClass()
some.m('a', 'b')
console.log('Calls to m: ', mMock.mock.calls)
Using that approach for A:
jest.mock('./A');
const A = require('./A');
A.mockImplementation((value) => {
return { value };
});
it('does stuff', () => {
const a = new A();
console.log(a); // -> A { value: 'value; }
});
The nice thing about that is you can do whatever you want to the returned value, like initialize .value.
The downsides are:
You don't get any automocking for free, e.g. I'd need to add .getValue() myself to the instance
You need to have a different jest.fn() mock function for each instance created, e.g. if I create two instances of A, each instance needs its own jest.fn() mock functions for the .getValue() method
SomeClass.mock.instances is not populated with the returned value (GitHub ticket)
One thing that didn't work (I was hoping that maybe Jest did some magic):
A.mockImplementation((value) => {
const rv = Object.create(A.prototype); // <- these are mocked methods
rv.value = value;
return rv;
});
Unfortunately, all instances share the same methods (as one would expect, but it was worth a shot).
My next step is to generate the mock, myself, via inspecting the prototype (I guess), but I wanted to see if there is an established approach.
Thanks in advance.

Turns out this is fixed (as of jest 24.1.0) and the code in the question works, as expected.
To recap, given class A:
A.js
module.exports = class A {
constructor(value) {
this.value = value;
}
setValue(value) {
this.value = value;
}
}
This test will now pass:
A.test.js
jest.mock('./A');
const A = require('./A');
A.mockImplementation((value) => {
const rv = Object.create(A.prototype); // <- these are mocked methods
rv.value = value;
return rv;
});
it('does stuff', () => {
const a = new A('some-value');
expect(A.mock.instances.length).toBe(1);
expect(a instanceof A).toBe(true);
expect(a).toEqual({ value: 'some-value' });
a.setValue('another-value');
expect(a.setValue.mock.calls.length).toBe(1);
expect(a.setValue.mock.calls[0]).toEqual(['another-value']);
});

The following worked for me:
A.mockImplementation(value => {
const rv = {value: value};
Object.setPrototypeOf(rv, A.prototype);
return rv
})

Related

Client-side typescript: how do I remove circular dependencies from the factory method pattern?

I am using the factory method pattern in some of my code. The problem is, some of those instances also use the same factory method pattern. This creates circular dependencies and I can't think of a way of removing them. Let me give an example:
// factoryMethod.ts
import Instance1 from './Instance1';
import Instance2 from './Instance2';
import Instance3 from './Instance3';
import { Instance, InstanceName } from './Instance';
export const getInstanceByName = (
instanceName: InstanceName
): Instance => {
switch (instanceName) {
case 'Instance1':
return Instance1;
case 'Instance2':
return Instance2;
case 'Instance3':
return Instance3;
default:
throw new Error();
}
};
// extremelyHelpfulUtilityFunction.ts
import { getInstanceByName } from './factoryMethod';
export const extremelyHelpfulUtilityFunction = (instanceName: InstanceName): number => {
// Imagine this was an extremely helpful utility function
return getInstanceByName(instanceName).num
}
// Instance.ts
export interface Instance {
doSomething: () => number;
num: number;
}
export type InstanceName = 'Instance1' | 'Instance2' | 'Instance3';
// Instance1.ts
import { extremelyHelpfulUtilityFunction } from './extremelyHelpfulUtilityFunction';
const i: Instance = {
doSomething: (): number => {
return extremelyHelpfulUtilityFunction('Instance2') + extremelyHelpfulUtilityFunction('Instance3'); // circular dependency
},
}
export default i;
// Other instances defined below, you get the idea.
I'm using rollup to turn this into a single JavaScript file, and when I do, it warns me that I have a circular dependency. I want to get rid of this warning. I realize the code will still function, but I don't want the warning there. How can I modify this code so that InstanceX can get InstanceY without it being a circular dependency?
IMO the problem is that extremelyHelpfulUtilityFunction has to know getInstanceByName, while the result of this factory could always be known in advance by the caller and the desired value passed as argument to the helper.
I would propose
// Instance1.ts
const instance1: Instance = {
doSomething: (): number => {
return (new Instance2()).toNum() + (new Instance3()).toNum()
},
}
with toNum defined in Instance.ts and overridden in its subclasses, using the helper but with proper parameters, for example
// Instance2.ts
const instance2: Instance = {
doSomething: ...,
toNum: (): number => {
return extremelyHelpfulUtilityFunction(1234)
}
}
where you would use this.num instead of 1234 if you declared a proper class for Instance2 instead of this object, like
// Instance2.ts
class Instance2 extends Instance {
num = 1234;
doSomething: ...
toNum(): number {
return extremelyHelpfulUtilityFunction(this.num)
}
}
export default new Instance2();

Unit Testing ES6 Class with External Dependency

I'm trying to set a unit testing boilerplate for my company. Our front end projects are built with ES6 classes and have a dependency to our core product. The front end code gets wrapped through a build process in a whole other block of code that is basically a closure and captures the dependency. So we don't have to manually import it in order to use it.
Let's say the dependency is called productScope and it's an object that has some DOM models, internal APIs and parameters among many other things necessary for each project. At the moment, Mocha throws ReferenceError: productScope is not defined. How can I mock this object? Or should I just use the actual object?
Example:
class someClass {
constructor() {
const id = productScope.items[0].id
const item = productScope.domModel.querySelector('.some-div')
item.classList.add(`added-${id}`)
}
}
This get wrapped in core code like below:
(function(productScope) {
// front end code goes here
}(productScope)
Testing file:
import someClass from '../../js/someClass'
describe('someClass', function() {
const someClass = new someClass()
it('should be a class', function() {
console.log(someClass)
});
});
You can try something like this
describe('#someClass', () => {
let someClass;
beforeEach(() => {
global.productScope = {
// mocking productScope object
};
});
it('should be a class', () => {
someClass = new SomeClass;
console.log(someClass);
});
afterEach(() => {
delete global.productScope;
});
});
or alternatively if you want more specific mock logic for each test case
describe('#someClass', () => {
let someClass;
it('should be a class', () => {
global.productScope = {
// mocking productScope object
};
// Test logic start
someClass = new SomeClass;
console.log(someClass);
// Test logic end
delete global.productScope;
});
});
Looks like productScope is a global variable.
Something like this should work for you.
import someClass from '../../js/someClass';
describe('someClass', function() {
let someClass;
beforeEach(() => {
global.productScope = {
// you mock definition
someClass = new someClass();
};
});
it('should be a class', function() {
console.log(someClass)
});
});
I'm with other answers as well, as managing global variables seems to be the simplest and most straightforward solution.
However, you can use toString to get class's string representation, and eval it to bind to closure's scope:
class someClass {
constructor() {
this.id = scopedId
}
}
// pass class as an argument
function scopeFactory(classDef) {
// define scoped data
let scopedId = 2;
// eval is used to bind class to the local closure
// so `scopedId` will be in charge
return eval("(" + classDef + ")");
}
const scopedSomeClass = scopeFactory(someClass);
console.log(new scopedSomeClass)
Note that eval(someCLass.toString()) doesn't work without parentheses.
You can add it as a helper function, into your project.

Generic reading of arguments from multiple constructor calls

Follow-up question to Read arguments from constructor call:
The accepted solution allows me to get arguments passed into a constructor by defining a wrapper class that captures and exposes the arguments, but this leaves me with the problem of having n wrappers for n constructors.
Is there a way to have 1 function/wrapper/whatever that could work for any number of constructors?
I'll reiterate that I'm pursing this technique specifically to test Webpack plugin configuration, and I'd like to avoid having a separate wrapper for each plugin that I need to test.
Looking for something along the lines of
// ------------------------------------------------------------ a wrapper function?
const someWrapper = () => { /* ... */ }
const plugin1 = new Plugin({ a: 'value' })
const plugin2 = new Plugin2(arg1, arg2, { b: 'anotherValue '})
someWrapper(plugin1).args === [{ a: 'value' }]
someWrapper(plugin2).args === [arg1, arg2, { b: 'anotherValue' }]
// --------------------------------------------------------------- a wrapper class?
class Wrapper { /* ... */ }
const plugin1 = new Wrapper(Plugin, [{ a: 'value' }])
const plugin2 = new Wrapper(Plugin2, [arg1, arg2, { b: 'anotherValue '}])
plugin1.args === [{ a: 'value' }]
plugin2.args === [arg1, arg2, { b: 'anotherValue '}]
// problem with above is the wrapper is being passed to Webpack, not the underlying
// plugin; not sure yet if this would cause webpack to break or not actually
// execute the plugin as intended with a vanilla config
// ---------------------------------------------------------------- something else?
Yes, you can create generic wrapper which will add args property to instance of any passed constructor:
class Plugin {
constructor (arg1, arg2) {
this.arg1 = arg1
this.arg2 = arg2
}
}
function wrapper(initial) {
// Rewrite initial constructor with our function
return function decoratedContructor(...args) {
// Create instance of initial object
const decorated = new initial(...args)
// Add some additional properties, methods
decorated.args = [...args]
// Return instantiated and modified object
return decorated
}
}
const decoratedPlugin = wrapper(Plugin)
const plugin = new decoratedPlugin('argument', { 'argument2': 1 })
console.log(plugin.args)
FYI: it's not safe to add properties without some prefix. Consider adding __ or something like this to your property, because you can accidentally rewrite some inner object property.
I was able to get this working with a modification to #guest271314's suggestion, namely, you need to pass ...initArgs to super(), otherwise webpack will fail with a TypeError: Cannot read property '...' of undefined.
Also took #terales's point into account about making sure to prefix my additional properties.
const exposeConstructorArgs = (Plugin, ...args) => {
const ExposedPlugin = class extends Plugin {
constructor(...initArgs) {
super(...initArgs);
this.__initArgs__ = initArgs;
}
get __initArgs() {
return this.__initArgs__;
}
};
return Reflect.construct(ExposedPlugin, args);
};
// ...
const dllPlugin = exposeConstructorArgs(webpack.DllPlugin, {
name: '[name]',
path: path.join(buildDir, '[name].json'),
});
// ...
const pluginConfig = dllPlugin.__initArgs[0];
expect(pluginConfig.name).toEqual('[name]');
You can use a generic function where class expression is used within function body. Pass reference to the class or constructor and parameters expected to be arguments within the instance to the function call.
function Plugin() {}
function Plugin2() {}
function PluginWrapper(pluginRef, ...args) {
let MyPlugin = class extends pluginRef {
constructor() {
super();
this.args = [...arguments];
}
getArgs() {
return this.args;
}
}
return Reflect.construct(MyPlugin, args);
};
const anInstance = PluginWrapper(Plugin, {
a: 'path'
});
console.log(anInstance.getArgs(), anInstance instanceof Plugin);
const aSecondInstance = PluginWrapper(Plugin2, "arg1", "arg2", {
b: 'anotherPath'
});
console.log(aSecondInstance.getArgs(), aSecondInstance instanceof Plugin2);

Unit testing Typescript decorators

I have an application built on typescript with decorators for some convenience property assignments and wondering how I can go about writing unit tests for them.
export function APIUrl() {
return function (target: any, key: string) {
let _value = target[key];
function getter() {
return _value;
}
function setter(newValue) {
_value = getApiURL();
}
if (delete target[key]) {
Object.defineProperty(target, key, {
get: getter,
set: setter
});
}
};
}
In a spec class I have,
it("should return url string", ()=> {
#APIUrl();
let baseURL:string;
expect(baseURL typeOf string).toBe(true)
})
Since decorators are just functions I would suggest to just test them like any other function. And only if you really need to, add one tests that shows how to use the decorator with a class/member/...
Here is an example such a test could look like:
import test from 'ava';
import { APIUrl } from './path';
const decorate = new APIUrl();
test.before(t => {
let obj = { someProp: 'foo' };
decorate(obj, 'someProp');
t.context.foo = obj;
});
test('should return original value', t => {
t.is(t.context.foo.someProp, 'foo');
});
Another approach could be to setup some properties and/or methods that use your decorators and test their usage directly.
Note: decorators can only be used on class methods and members so you'd need to create a dummy class in your test.
Here's an example:
//Test Setup
class Test {
#APIUrl()
url: string;
#AnotherDecorator()
anotherFunction() {}
}
//Unit tests
describe('Decorator Tests', () => {
it('should work', () => {
const t = new Test();
expect(t.url).toEqual("something");
expect(t.anotherFunction()).toReturn("something else");
});
}

How to mock localStorage in JavaScript unit tests?

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

Categories

Resources