Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I deal with localStorage in jest tests?

Tags:

jestjs

I keep getting "localStorage is not defined" in Jest tests which makes sense but what are my options? Hitting brick walls.

like image 421
Chiedo Avatar asked Oct 02 '15 16:10

Chiedo


People also ask

Does jest have localStorage?

In short: localStorage is actually an instance of a Storage class. Sadly, mocking methods on a class instance (i.e. localStorage. getItem ) doesn't work with our jest.

How do I use jest localStorage mock?

To mock local storage in Jest tests, we can create our own mock local storage methods. const localStorageMock = (() => { let store = {}; return { getItem(key) { return store[key]; }, setItem(key, value) { store[key] = value. toString(); }, clear() { store = {}; }, removeItem(key) { delete store[key]; } }; })(); Object.

How do I test local storage?

Using Google Chrome, click on menu -> Tools -> Developer Tools. Then under Resources you will see 'Local Storage' and 'Web Storage'. Using Firefox with the Firebug add on you can easily inspect the localStorage/sessionStorage object in the DOM tab.

How do you use spyOn function in jest?

To spy on an exported function in jest, you need to import all named exports and provide that object to the jest. spyOn function. That would look like this: import * as moduleApi from '@module/api'; // Somewhere in your test case or test suite jest.


22 Answers

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;
like image 124
nickcan Avatar answered Oct 12 '22 23:10

nickcan


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",

like image 32
Chiedo Avatar answered Oct 13 '22 00:10

Chiedo


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:
jest.spyOn(Storage.prototype, 'setItem');
Storage.prototype.setItem = jest.fn();

// assertions as usual:
expect(localStorage.setItem).toHaveBeenCalled();
like image 44
Bastian Stein Avatar answered Oct 13 '22 00:10

Bastian Stein


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

like image 33
c4k Avatar answered Oct 13 '22 00:10

c4k


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');
like image 27
Christian Saiki Avatar answered Oct 13 '22 00:10

Christian Saiki


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
like image 20
Dmitriy Avatar answered Oct 13 '22 01:10

Dmitriy


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.

like image 39
Aligertor Avatar answered Oct 13 '22 00:10

Aligertor


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');
like image 37
TigerBear Avatar answered Oct 13 '22 00:10

TigerBear


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.

like image 34
Carlos Huamani Avatar answered Oct 13 '22 00:10

Carlos Huamani


I thought I would add another solution that worked very neatly for me in TypeScript w/ React:

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')
    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 = "value"
    const key = "key"
    render(<Component />);
    expect(setItemMock).toHaveBeenCalledWith(key, value);
}
like image 37
Jamie Avatar answered Oct 12 '22 23:10

Jamie


You can use this approach, to avoid mocking.

Storage.prototype.getItem = jest.fn(() => expectedPayload);
like image 33
Sanath Avatar answered Oct 12 '22 23:10

Sanath


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)
      })
    })
like image 22
Greg Wozniak Avatar answered Oct 13 '22 00:10

Greg Wozniak


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");
like image 21
Vlad Moisuc Avatar answered Oct 13 '22 00:10

Vlad Moisuc


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

like image 22
Mad-D Avatar answered Oct 13 '22 00:10

Mad-D


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.

like image 41
vs_lala Avatar answered Oct 12 '22 23:10

vs_lala


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

like image 32
Trevor Joseph Avatar answered Oct 13 '22 00:10

Trevor Joseph


This worked for me and just one code line

const setItem = jest.spyOn(Object.getPrototypeOf(localStorage), 'setItem');

like image 27
Kevin Mendez Avatar answered Oct 13 '22 00:10

Kevin Mendez


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();
like image 22
John Avatar answered Oct 12 '22 23:10

John


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.

like image 43
CorayThan Avatar answered Oct 13 '22 00:10

CorayThan


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.

like image 45
schlingel Avatar answered Oct 13 '22 00:10

schlingel


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

like image 43
Beau Smith Avatar answered Oct 13 '22 01:10

Beau Smith


As mentioned in a comment by Niket Pathak, starting jest@24 / [email protected] and above, localStorage is mocked automatically.

like image 21
Marius Butuc Avatar answered Oct 13 '22 01:10

Marius Butuc