calling ramda compose in nodejs class - node.js

I have the following method skipLoggingThisRequest in a node js class which I am trying to test. The method is supposed to return either true or false, based on the path in request, using ramda compose to get to that value. However in my tests, no matter what path I set in the request object, my skipLoggingThisRequest always returns true.
What am I missing here?
my class:
import { compose, filter, join, toPairs, map, prop, flip, contains, test, append } from 'ramda'
import { create, env } from 'sanctuary'
import { isEmpty, flattenDeep } from 'lodash'
import chalk from 'chalk'
import log from 'menna'
class MyClass {
constructor (headerList) {
this.headerWhiteList = flattenDeep(append(headerList, []));
}
static getBody (req) {
return (!isEmpty(req.body) ? JSON.stringify(req.body) : '');
}
static S () {
return create({ checkTypes: false, env });
}
static isInList () {
return flip(contains);
}
static isInWhitelist () {
return compose(this.isInList(this.headerWhiteList), this.S.maybeToNullable, this.S.head);
}
static parseHeaders () {
return (req) => compose(join(','), map(join(':')), filter(this.isInWhitelist), toPairs, prop('headers'));
}
skipLoggingThisRequest () {
return (req) => compose(test(/^.*(swagger|docs|health).*$/), prop('path'))
}
logger (req, res, next) {
if (this.skipLoggingThisRequest(req)) {
console.log('Skipping')
return next();
}
const primaryText = chalk.inverse(`${req.ip} ${req.method} ${req.originalUrl}`);
const secondaryText = chalk.gray(`${this.parseHeaders(req)} ${this.getBody(req)}`);
log.info(`${primaryText} ${secondaryText}`);
return next();
}
}
export default MyClass
My tests:
import sinon from 'sinon';
import MyClass from '../lib/MyClass';
describe('MyClass', () => {
const headerList = ['request-header-1', 'request-header-2'];
const request = {
'headers': {
'request-header-1': 'yabadaba',
'request-header-2': 'dooooooo'
},
'ip': 'shalalam',
'method': 'GET',
'originalUrl': 'http://myOriginalUrl.com',
'body': ''
};
const response = {};
const nextStub = sinon.stub();
describe('Logs request', () => {
const myInstance = new MyClass(headerList);
const skipLogSpy = sinon.spy(myInstance, 'skipLoggingThisRequest');
request.path = '/my/special/path';
myInstance.logger(request, response, nextStub);
sinon.assert.called(nextStub);
});
});

this.skipLoggingThisRequest(req) returns a function ((req) => compose(test(/^.*(swagger|docs|health).*$/), prop('path'))).
It does not return a boolean. However, since functions are truthy, your if statement always executes.
What you most likely want to do is this.skipLoggingThisRequest()(req). You get the function and then apply a request to it.
Demonstration of what's going on:
const testFunction = () => (test) => test === "Hello!";
console.log(testFunction);
console.log(testFunction());
console.log(testFunction()("Hello!"));
console.log(testFunction()("Goodbye!"));
if (testFunction) {
console.log("testFunction is truthy.");
}
if (testFunction()) {
console.log("testFunction() is truthy.");
}
if (testFunction()("Hello!")) {
console.log('testFunction()("Hello!") is truthy.');
}
if (!testFunction()("Goodbye!")) {
console.log('testFunction()("Goodbye!") is falsey.');
}

Related

Typescript: cannot load json file contents into array

I need load my apikeys from json file (api.json) with file structure
api.json:
{
"service1": ["apikey1", "apikey2"],
"service2": ["apikey1", "apikey2"],
}
I have a class that describes a key & value:
class ApiKey {
constructor(apiName: String, apiKeys: Array<String>)
}
And I have a class, that loads all keys from file to Array:
//read file async: https://stackoverflow.com/questions/46867517/how-to-read-file-with-async-await-properly
import { readFile } from 'fs'
import { promisify } from 'util'
import { join } from 'path'
export class ApikeysService {
constructor(private apiKeys: Array<ApiKey> = new Array<ApiKey>()) {
this.loadKeys()
}
public loadKeys () {
const filePath = join(__dirname, "../../../", "api.json");
const readfile = promisify(readFile);
const result = readfile(filePath, "utf8")
.then(content => JSON.parse(content))
.then(result => {
Object.keys(result).forEach(key => {
this.apikeys.push(new ApiKey(key, result[key]))
})
})
}
public getKeyFor(name: String) {
return this.keyCounters.find(x => x.keyname == name).apiKeys
}
}
And my tests:
describe('should load api keys', () => {
it("should load apikeys from file", async () => {
const service = new ApikeysService ()
it("should load api keys for service1", () => {
expect(service.getKeyFor("service1")[0]).toEqual("apikey1")
expect(service.getKeyFor("service1")[1]).toEqual("apikey2")
})
})
Test Result:
expect(received).toEqual(expected) // deep equality
> Expected: "apikey1"
> Received: undefined
I tried i lot of different ways to load contents from file to array in class (async also) but it wont work
You are firing an async function in your constructor, and checking the result directly afterwards - you give no chance for the promise to resolve. It is not synchronous.
Either return result from loadKeys and make sure to await it, or change it to become synchronous, using readFileSync.
Example 1
export class ApikeysService {
constructor(private apiKeys: Array<ApiKey> = new Array<ApiKey>()) {
this.loadKeys()
}
public loadKeys () {
const filePath = join(__dirname, "../../../", "api.json");
const readfile = promisify(readFile);
const content = JSON.parse(readFileSync(filePath, "utf8"));
Object.keys(result).forEach(key => {
this.apikeys.push(new ApiKey(key, result[key]))
})
}
public getKeyFor(name: String) {
return this.keyCounters.find(x => x.keyname == name).apiKeys
}
}
Example 2
export class ApikeysService {
constructor(private apiKeys: Array<ApiKey> = new Array<ApiKey>()) {
// this.loadKeys()
}
public loadKeys () {
const filePath = join(__dirname, "../../../", "api.json");
const readfile = promisify(readFile);
const result = readfile(filePath, "utf8")
.then(content => JSON.parse(content))
.then(result => {
Object.keys(result).forEach(key => {
this.apikeys.push(new ApiKey(key, result[key]))
})
})
return result;
}
public getKeyFor(name: String) {
return this.keyCounters.find(x => x.keyname == name).apiKeys
}
}
describe('should load api keys', () => {
it("should load apikeys from file", async () => {
const service = new ApikeysService ()
await service.loadKeys();
it("should load api keys for service1", () => {
expect(service.getKeyFor("service1")[0]).toEqual("apikey1")
expect(service.getKeyFor("service1")[1]).toEqual("apikey2")
})
})
})

Tests haphazardly fail when using graphql-request and MSW

I am trying to set up tests to test a Remix loader and noticed that the request function from graphql-request haphazardly fails when using MSW. If I replace this and use a simple fetch for the requests the tests pass.
Is there any configuration I need to change? I have created a sample repo that demonstrates the bug. The bug gets worse the more tests you have that are using the same mocked request.
Sample repo: https://github.com/charklewis/sb9or3
Here is a summary of the code I am using:
//modules/database.server
import { GraphQLClient } from "graphql-request";
const client = new GraphQLClient("http://some-graphql-api.com/api/graphql", {});
export const fetchQuery = async (query: any, variables: any) => {
try {
const response = await client.request(query, variables || {});
return response;
} catch (error) {
return {};
}
};
//routes/index
import type { LoaderFunction } from "remix";
import { json } from "remix";
import { gql } from "graphql-request";
import { fetchQuery } from "~/modules/database.server";
export const loader: LoaderFunction = async () => {
const query = gql`
query MyQuery {
demoQuery {
value
}
}
`;
const response = await fetchQuery(query, {});
return json({ value: response.demoQuery.value });
};
//routes/__tests__/index
import { graphql } from "msw";
import { setupServer } from "msw/node";
import { loader } from "~/routes/index";
const createServer = (handlers: any[] = []) => {
const server = setupServer(...handlers);
beforeAll(() => server.listen({ onUnhandledRequest: "bypass" }));
afterAll(() => server.close());
afterEach(() => server.resetHandlers());
return server;
};
const createDemoQueryHandler = ({ value = true } = {}) => {
return graphql.query("MyQuery", (req, res, ctx) => {
return res(ctx.data({ demoQuery: { value } }));
});
};
createServer([createDemoQueryHandler()]);
test("the loader returns data (round 1)", async () => {
const response = await loader({
request: new Request("/", { method: "GET" }),
params: {},
context: {},
});
const data = await response.json();
expect(data.value).toBe(true);
});
My vitest configuration is:
/// <reference types="vitest" />
/// <reference types="vite/client" />
import { defineConfig } from "vite";
import react from "#vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [react(), tsconfigPaths()],
test: {
globals: true,
environment: "jsdom",
setupFiles: "./app/test-utils.ts",
testTimeout: 20000
}
});

Why when I tried to execute following test code, I got "The first character of a path should be `/` or `*`" error?

When I tried to execute following test code, I got error like "The first character of a path should be / or *"
archives.test.ts
import ...
describe('Get All Archived', () => {
test('it must return ok', async () => {
const app = init()
const response = await app.inject().get('/client/archives')
const body = JSON.parse(response.body)
expect(response.statusCode).toBe(200)
expect(body.status).toBe('ok')
expect(body.data.length).toBe(0)
})
})
My code:
api.ts
import fastify, { FastifyInstance } from 'fastify'
other imports...
export const init = (): FastifyInstance => {
const app = fastify()
app.addContentTypeParser('application/json', { parseAs: 'string' }, parseApplicationJson)
app.register(ArchivesPlugin)
return app
}
const proxy = awsLambdaFastify(init())
export const handler = proxy
ArchivesPlugin.ts
import fp from 'fastify-plugin'
import { FastifyInstance, FastifyPluginAsync } from 'fastify'
import { register } from '../controllers/archives'
const ArchivesPlugin: FastifyPluginAsync = async (app: FastifyInstance) => {
await register(app)
}
export default fp(ArchivesPlugin)
register.ts
import { FastifyInstance, FastifyRequest } from 'fastify'
other imports...
const handlers = {
code content
},
}
export async function register(app: FastifyInstance) {
app.route({
method: 'GET',
url: `${prefix}/archives`,
schema: { ...GetArchiveListResponse, tags: ['archives'] },
handler: handlers.index,
})
}
Appreciate any help.
The problem was caused by ${prefix} in register.ts which is given above. When called register.ts, prefix seemed undefined. That's why I was getting the error.

Thunk + Redux (in React Native) : Can't get action to work

I'm brand new to this stack. I've seen quite a few other questions about this and have read the Thunk documentation but I can't stitch this together.
When I run the code below I get the error "Actions must be plain objects, use custom middleware for async actions" which is exactly the problem I'm trying to solve with Thunk.
My action looks like this:
src/actions/recipes.js
// this calls the API
function fetchApiGetRecipes() {
return fetch('https://mywebsite.com/endpoint/', {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + idToken
}
}).then((json) => {
dispatch({
type: 'RECIPES_REPLACE',
data: json
})
});
}
// this is passed into my container to use to refresh the recipe list
export function getRecipes() {
if (Firebase === null) return () => new Promise(resolve => resolve());
if (Firebase.auth().currentUser !== null) {
Firebase.auth().currentUser.getIdToken(/* forceRefresh */ true).then(function(idToken) {
// console.log(idToken);
return dispatch => new Promise(resolve => fetchApiGetRecipes(idToken) )
}).catch(function(error) {
// Handle error
});
} else {
console.log("Null user");
}
}
What is the correct syntax to use Thunk here and fix the error I'm getting when the app starts up?
EDIT: I create the store like this:
import { createStore, applyMiddleware, compose } from 'redux';
import { persistStore, persistCombineReducers } from 'redux-persist';
import storage from 'redux-persist/es/storage'; // default: localStorage if web, AsyncStorage if react-native
import thunk from 'redux-thunk';
import reducers from '../reducers';
// Redux Persist config
const config = {
key: 'root',
storage,
blacklist: ['status'],
};
const reducer = persistCombineReducers(config, reducers);
const middleware = [thunk];
const configureStore = () => {
const store = createStore(
reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__(),
compose(applyMiddleware(...middleware)),
);
const persistor = persistStore(
store,
null,
() => { store.getState(); },
);
return { persistor, store };
};
export default configureStore;
Your getRecipes function doesn't return a function in the if (Firebase.auth().currentUser !== null) clause.
You need to return a function where you are just doing
Firebase.auth().currentUser.getIdToken(/* forceRefresh */ true).then(function(idToken) {
// console.log(idToken);
return dispatch => new Promise(resolve => fetchApiGetRecipes(idToken) )
}).catch(function(error) {
// Handle error
});
The dispatch function (I assume is the intended one to return) is being returned in the then clause of the promise. That doesn't return the dispatch function to the outer method getRecipies. Hence the error
probably you forgot pass dispatch to func args?
// you use dispatch in this func
function fetchApiGetRecipes() {...}
// you forget pass
return dispatch => new Promise(resolve => fetchApiGetRecipes(idToken) )

How do I deal with localStorage in jest tests?

I keep getting "localStorage is not defined" in Jest tests which makes sense but what are my options? Hitting brick walls.
Great solution from #chiedo
However, we use ES2015 syntax and I felt it was a little cleaner to write it this way.
class LocalStorageMock {
constructor() {
this.store = {};
}
clear() {
this.store = {};
}
getItem(key) {
return this.store[key] || null;
}
setItem(key, value) {
this.store[key] = String(value);
}
removeItem(key) {
delete this.store[key];
}
}
global.localStorage = new LocalStorageMock;
Figured it out with help from this: https://groups.google.com/forum/#!topic/jestjs/9EPhuNWVYTg
Setup a file with the following contents:
var localStorageMock = (function() {
var store = {};
return {
getItem: function(key) {
return store[key];
},
setItem: function(key, value) {
store[key] = value.toString();
},
clear: function() {
store = {};
},
removeItem: function(key) {
delete store[key];
}
};
})();
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
Then you add the following line to your package.json under your Jest configs
"setupTestFrameworkScriptFile":"PATH_TO_YOUR_FILE",
Answer:
Currently (Jul '22) localStorage can not be mocked or spied on by jest as you usually would, and as outlined in the create-react-app docs. This is due to changes made in jsdom. You can read about it in the jest and jsdom issue trackers.
As a workaround, you can spy on the prototype instead:
// does not work:
jest.spyOn(localStorage, "setItem");
localStorage.setItem = jest.fn();
// either of these lines will work, different syntax that does the same thing:
jest.spyOn(Storage.prototype, 'setItem');
Storage.prototype.setItem = jest.fn();
// assertions as usual:
expect(localStorage.setItem).toHaveBeenCalled();
A note on spying on the prototype:
Spying on an instance gives you the ability to observe and mock behaviour for a specific object.
Spying on the prototype, on the other hand, will observe/manipulate every instance of that class all at once. Unless you have a special usecase, this is probably not what you want.
However, in this case it makes no difference, because there only exists a single instance of localStorage.
If using create-react-app, there is a simpler and straightforward solution explained in the documentation.
Create src/setupTests.js and put this in it :
const localStorageMock = {
getItem: jest.fn(),
setItem: jest.fn(),
clear: jest.fn()
};
global.localStorage = localStorageMock;
Tom Mertz contribution in a comment below :
You can then test that your localStorageMock's functions are used by doing something like
expect(localStorage.getItem).toBeCalledWith('token')
// or
expect(localStorage.getItem.mock.calls.length).toBe(1)
inside of your tests if you wanted to make sure it was called. Check out https://facebook.github.io/jest/docs/en/mock-functions.html
Unfortunately, the solutions that I've found here didn't work for me.
So I was looking at Jest GitHub issues and found this thread
The most upvoted solutions were these ones:
const spy = jest.spyOn(Storage.prototype, 'setItem');
// or
Storage.prototype.getItem = jest.fn(() => 'bla');
A better alternative which handles undefined values (it doesn't have toString()) and returns null if value doesn't exist. Tested this with react v15, redux and redux-auth-wrapper
class LocalStorageMock {
constructor() {
this.store = {}
}
clear() {
this.store = {}
}
getItem(key) {
return this.store[key] || null
}
setItem(key, value) {
this.store[key] = value
}
removeItem(key) {
delete this.store[key]
}
}
global.localStorage = new LocalStorageMock
or you just take a mock package like this:
https://www.npmjs.com/package/jest-localstorage-mock
it handles not only the storage functionality but also allows you test if the store was actually called.
If you are looking for a mock and not a stub, here is the solution I use:
export const localStorageMock = {
getItem: jest.fn().mockImplementation(key => localStorageItems[key]),
setItem: jest.fn().mockImplementation((key, value) => {
localStorageItems[key] = value;
}),
clear: jest.fn().mockImplementation(() => {
localStorageItems = {};
}),
removeItem: jest.fn().mockImplementation((key) => {
localStorageItems[key] = undefined;
}),
};
export let localStorageItems = {}; // eslint-disable-line import/no-mutable-exports
I export the storage items for easy initialization. I.E. I can easily set it to an object
In the newer versions of Jest + JSDom it is not possible to set this, but the localstorage is already available and you can spy on it it like so:
const setItemSpy = jest.spyOn(Object.getPrototypeOf(window.localStorage), 'setItem');
For Jest, React & TypeScript users:
I created a mockLocalStorage.ts
export const mockLocalStorage = () => {
const setItemMock = jest.fn();
const getItemMock = jest.fn();
beforeEach(() => {
Storage.prototype.setItem = setItemMock;
Storage.prototype.getItem = getItemMock;
});
afterEach(() => {
setItemMock.mockRestore();
getItemMock.mockRestore();
});
return { setItemMock, getItemMock };
};
My component:
export const Component = () => {
const foo = localStorage.getItem('foo')
localStorage.setItem('bar', 'true')
return <h1>{foo}</h1>
}
then in my tests I use it like so:
import React from 'react';
import { mockLocalStorage } from '../../test-utils';
import { Component } from './Component';
const { getItemMock, setItemMock } = mockLocalStorage();
it('fetches something from localStorage', () => {
getItemMock.mockReturnValue('bar');
render(<Component />);
expect(getItemMock).toHaveBeenCalled();
expect(getByText(/bar/i)).toBeInTheDocument()
});
it('expects something to be set in localStorage' () => {
const value = "true"
const key = "bar"
render(<Component />);
expect(setItemMock).toHaveBeenCalledWith(key, value);
}
I found this solution from github
var localStorageMock = (function() {
var store = {};
return {
getItem: function(key) {
return store[key] || null;
},
setItem: function(key, value) {
store[key] = value.toString();
},
clear: function() {
store = {};
}
};
})();
Object.defineProperty(window, 'localStorage', {
value: localStorageMock
});
You can insert this code in your setupTests and it should work fine.
I tested it in a project with typesctipt.
A bit more elegant solution using TypeScript and Jest.
interface Spies {
[key: string]: jest.SpyInstance
}
describe('→ Local storage', () => {
const spies: Spies = {}
beforeEach(() => {
['setItem', 'getItem', 'clear'].forEach((fn: string) => {
const mock = jest.fn(localStorage[fn])
spies[fn] = jest.spyOn(Storage.prototype, fn).mockImplementation(mock)
})
})
afterEach(() => {
Object.keys(spies).forEach((key: string) => spies[key].mockRestore())
})
test('→ setItem ...', async () => {
localStorage.setItem( 'foo', 'bar' )
expect(localStorage.getItem('foo')).toEqual('bar')
expect(spies.setItem).toHaveBeenCalledTimes(1)
})
})
You can use this approach, to avoid mocking.
Storage.prototype.getItem = jest.fn(() => expectedPayload);
Object.defineProperty(window, "localStorage", {
value: {
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
},
});
or
jest.spyOn(Object.getPrototypeOf(localStorage), "getItem");
jest.spyOn(Object.getPrototypeOf(localStorage), "setItem");
As #ck4 suggested documentation has clear explanation for using localStorage in jest. However the mock functions were failing to execute any of the localStorage methods.
Below is the detailed example of my react component which make uses of abstract methods for writing and reading data,
//file: storage.js
const key = 'ABC';
export function readFromStore (){
return JSON.parse(localStorage.getItem(key));
}
export function saveToStore (value) {
localStorage.setItem(key, JSON.stringify(value));
}
export default { readFromStore, saveToStore };
Error:
TypeError: _setupLocalStorage2.default.setItem is not a function
Fix:
Add below mock function for jest (path: .jest/mocks/setUpStore.js )
let mockStorage = {};
module.exports = window.localStorage = {
setItem: (key, val) => Object.assign(mockStorage, {[key]: val}),
getItem: (key) => mockStorage[key],
clear: () => mockStorage = {}
};
Snippet is referenced from here
To do the same in the Typescript, do the following:
Setup a file with the following contents:
let localStorageMock = (function() {
let store = new Map()
return {
getItem(key: string):string {
return store.get(key);
},
setItem: function(key: string, value: string) {
store.set(key, value);
},
clear: function() {
store = new Map();
},
removeItem: function(key: string) {
store.delete(key)
}
};
})();
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
Then you add the following line to your package.json under your Jest configs
"setupTestFrameworkScriptFile":"PATH_TO_YOUR_FILE",
Or you import this file in your test case where you want to mock the localstorage.
describe('getToken', () => {
const Auth = new AuthService();
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ik1yIEpvc2VwaCIsImlkIjoiNWQwYjk1Mzg2NTVhOTQ0ZjA0NjE5ZTA5IiwiZW1haWwiOiJ0cmV2X2pvc0Bob3RtYWlsLmNvbSIsInByb2ZpbGVVc2VybmFtZSI6Ii9tcmpvc2VwaCIsInByb2ZpbGVJbWFnZSI6Ii9Eb3Nlbi10LUdpci1sb29rLWN1dGUtbnVrZWNhdDMxNnMtMzExNzAwNDYtMTI4MC04MDAuanBnIiwiaWF0IjoxNTYyMzE4NDA0LCJleHAiOjE1OTM4NzYwMDR9.YwU15SqHMh1nO51eSa0YsOK-YLlaCx6ijceOKhZfQZc';
beforeEach(() => {
global.localStorage = jest.fn().mockImplementation(() => {
return {
getItem: jest.fn().mockReturnValue(token)
}
});
});
it('should get the token from localStorage', () => {
const result = Auth.getToken();
expect(result).toEqual(token);
});
});
Create a mock and add it to the global object
At least as of now, localStorage can be spied on easily on your jest tests, for example:
const spyRemoveItem = jest.spyOn(window.localStorage, 'removeItem')
And that's it. You can use your spy as you are used to.
This worked for me and just one code line
const setItem = jest.spyOn(Object.getPrototypeOf(localStorage), 'setItem');
2021, typescript
class LocalStorageMock {
store: { [k: string]: string };
length: number;
constructor() {
this.store = {};
this.length = 0;
}
/**
* #see https://developer.mozilla.org/en-US/docs/Web/API/Storage/key
* #returns
*/
key = (idx: number): string => {
const values = Object.values(this.store);
return values[idx];
};
clear() {
this.store = {};
}
getItem(key: string) {
return this.store[key] || null;
}
setItem(key: string, value: string) {
this.store[key] = String(value);
}
removeItem(key: string) {
delete this.store[key];
}
}
export default LocalStorageMock;
you can then use it with
global.localStorage = new LocalStorageMock();
Riffed off some other answers here to solve it for a project with Typescript. I created a LocalStorageMock like this:
export class LocalStorageMock {
private store = {}
clear() {
this.store = {}
}
getItem(key: string) {
return this.store[key] || null
}
setItem(key: string, value: string) {
this.store[key] = value
}
removeItem(key: string) {
delete this.store[key]
}
}
Then I created a LocalStorageWrapper class that I use for all access to local storage in the app instead of directly accessing the global local storage variable. Made it easy to set the mock in the wrapper for tests.
As mentioned in a comment by Niket Pathak,
starting jest#24 / jsdom#11.12.0 and above, localStorage is mocked automatically.
An update for 2022.
Jest#24+ has ability to mock local storage automatically. However, the dependency needed no longer ships with it by default.
npm i -D jest-environment-jsdom
You also need to change your Jest test mode:
// jest.config.cjs
module.exports = {
...
testEnvironment: "jsdom",
...
};
Now localStorage will already be mocked for you.
Example:
// myStore.js
const saveLocally = (key, value) => {
localStorage.setItem(key, value)
};
Test:
// myStore.spec.ts
import { saveLocally } from "./myStore.js"
it("saves key-value pair", () => {
let key = "myKey";
let value = "myValue";
expect(localStorage.getItem(key)).toBe(null);
saveLocally(key, value);
expect(localStorage.getItem(key)).toBe(value);
};
The following solution is compatible for testing with stricter TypeScript, ESLint, TSLint, and Prettier config: { "proseWrap": "always", "semi": false, "singleQuote": true, "trailingComma": "es5" }:
class LocalStorageMock {
public store: {
[key: string]: string
}
constructor() {
this.store = {}
}
public clear() {
this.store = {}
}
public getItem(key: string) {
return this.store[key] || undefined
}
public setItem(key: string, value: string) {
this.store[key] = value.toString()
}
public removeItem(key: string) {
delete this.store[key]
}
}
/* tslint:disable-next-line:no-any */
;(global as any).localStorage = new LocalStorageMock()
HT/ https://stackoverflow.com/a/51583401/101290 for how to update global.localStorage
There is no need to mock localStorage - just use the jsdom environment so that your tests run in browser-like conditions.
In your jest.config.js,
module.exports = {
// ...
testEnvironment: "jsdom"
}
none of the answers above worked for me. So after some digging this is what I got to work. Credit goes to a few sources and other answers as well.
https://www.codeblocq.com/2021/01/Jest-Mock-Local-Storage/
https://github.com/facebook/jest/issues/6798#issuecomment-440988627
https://gist.github.com/mayank23/7b994385eb030f1efb7075c4f1f6ac4c
https://github.com/facebook/jest/issues/6798#issuecomment-514266034
My full gist: https://gist.github.com/ar-to/01fa07f2c03e7c1b2cfe6b8c612d4c6b
/**
* Build Local Storage object
* #see https://www.codeblocq.com/2021/01/Jest-Mock-Local-Storage/ for source
* #see https://stackoverflow.com/a/32911774/9270352 for source
* #returns
*/
export const fakeLocalStorage = () => {
let store: { [key: string]: string } = {}
return {
getItem: function (key: string) {
return store[key] || null
},
setItem: function (key: string, value: string) {
store[key] = value.toString()
},
removeItem: function (key: string) {
delete store[key]
},
clear: function () {
store = {}
},
}
}
/**
* Mock window properties for testing
* #see https://gist.github.com/mayank23/7b994385eb030f1efb7075c4f1f6ac4c for source
* #see https://github.com/facebook/jest/issues/6798#issuecomment-514266034 for sample implementation
* #see https://developer.mozilla.org/en-US/docs/Web/API/Window#properties for window properties
* #param { string } property window property string but set to any due to some warnings
* #param { Object } value for property
*
* #example
*
* const testLS = {
* id: 5,
* name: 'My Test',
* }
* mockWindowProperty('localStorage', fakeLocalStorage())
* window.localStorage.setItem('currentPage', JSON.stringify(testLS))
*
*/
const mockWindowProperty = (property: string | any, value: any) => {
const { [property]: originalProperty } = window
delete window[property]
beforeAll(() => {
Object.defineProperty(window, property, {
configurable: true,
writable: true,
value,
})
})
afterAll(() => {
window[property] = originalProperty
})
}
export default mockWindowProperty
In my case, I needed to set the localStorage value before I check it.
So what I did is
const data = { .......}
const setLocalStorageValue = (name: string, value: any) => {
localStorage.setItem(name, JSON.stringify(value))
}
describe('Check X class', () => {
setLocalStorageValue('Xname', data)
const xClass= new XClass()
console.log(xClass.initiate()) ; // it will work
})
2022 December: Nx 14 with Angular 14 Jest.
We have an test-setup.ts file in every app and libs folder. We setting local storage mock globaly.
import 'jest-preset-angular/setup-jest';
Storage.prototype.getItem = jest.fn();
Storage.prototype.setItem = jest.fn();
Storage.prototype.removeItem = jest.fn();
Then localStorage.service.spec.ts file looking like this:
import { LocalStorageService } from './localstorage.service';
describe('LocalStorageService', () => {
let localStorageService: LocalStorageService;
beforeEach(() => {
localStorageService = new LocalStorageService();
});
it('should set "identityKey" in localStorage', async () => {
localStorageService.saveData('identityKey', '99');
expect(window.localStorage.setItem).toHaveBeenCalled();
expect(window.localStorage.setItem).toHaveBeenCalledWith('identityKey', '99');
expect(window.localStorage.setItem).toHaveBeenCalledTimes(1);
});
it('should get "identityKey" from localStorage', async () => {
localStorageService.getData('identityKey');
expect(window.localStorage.getItem).toHaveBeenCalled();
expect(window.localStorage.getItem).toHaveBeenCalledWith('identityKey');
expect(window.localStorage.getItem).toHaveBeenCalledTimes(1);
});
it('should remove "identityKey" from localStorage', async () => {
localStorageService.removeData('identityKey');
expect(window.localStorage.removeItem).toHaveBeenCalled();
expect(window.localStorage.removeItem).toHaveBeenCalledWith('identityKey');
expect(window.localStorage.removeItem).toHaveBeenCalledTimes(1);
});
});
P.S. Sorry for bad indentation, this SatckOverflow window s*cks.
First: I created a file named localStorage.ts(localStorage.js)
class LocalStorageMock {
store: Store;
length: number;
constructor() {
this.store = {};
this.length = 0;
}
key(n: number): any {
if (typeof n === 'undefined') {
throw new Error(
"Uncaught TypeError: Failed to execute 'key' on 'Storage': 1 argument required, but only 0 present."
);
}
if (n >= Object.keys(this.store).length) {
return null;
}
return Object.keys(this.store)[n];
}
getItem(key: string): Store | null {
if (!Object.keys(this.store).includes(key)) {
return null;
}
return this.store[key];
}
setItem(key: string, value: any): undefined {
if (typeof key === 'undefined' && typeof value === 'undefined') {
throw new Error(
"Uncaught TypeError: Failed to execute 'setItem' on 'Storage': 2 arguments required, but only 0 present."
);
}
if (typeof value === 'undefined') {
throw new Error(
"Uncaught TypeError: Failed to execute 'setItem' on 'Storage': 2 arguments required, but only 1 present."
);
}
if (!key) return undefined;
this.store[key] = value.toString() || '';
this.length = Object.keys(this.store).length;
return undefined;
}
removeItem(key: string): undefined {
if (typeof key === 'undefined') {
throw new Error(
"Uncaught TypeError: Failed to execute 'removeItem' on 'Storage': 1 argument required, but only 0 present."
);
}
delete this.store[key];
this.length = Object.keys(this.store).length;
return undefined;
}
clear(): undefined {
this.store = {};
this.length = 0;
return undefined;
}
}
export const getLocalStorageMock = (): any => {
return new LocalStorageMock();
};
global.localStorage = new LocalStorageMock();
Then create a test file named session.test.ts(session.test.js)
import { getLocalStorageMock } from '../localstorage';
describe('session storage', () => {
let localStorage;
beforeEach(() => {
localStorage = getLocalStorageMock();
});
describe('getItem', () => {
it('should return null if the item is undefined', () => {
expect(localStorage.getItem('item')).toBeNull();
});
it("should return '' instead of null", () => {
localStorage.setItem('item', '');
expect(localStorage.getItem('item')).toBe('');
});
it('should return navid', () => {
localStorage.setItem('item', 'navid');
expect(localStorage.getItem('item')).toBe('navid');
});
});
});
This worked for me,
delete global.localStorage;
global.localStorage = {
getItem: () =>
}

Resources