0

in my react + redux app I have an action that besides returning type and payload also changes localStorage like:

export const cancel = () => {
  localStorage.removeItem("MyAppData");
  return { type: types.USER_CANCEL};
};

my test looks like:

test("user cancels", () => {
  const action = actions.cancel();
  expect(action).toEqual({
    type: types.USER_CANCEL
  });
});

writing no test for local storage I get

  ● user cancels

    ReferenceError: localStorage is not defined

      33 |
      34 | export const cancel = () => {
    > 35 |   localStorage.removeItem("MyAppData");
         |   ^
      36 |   return { type: types.USER_CANCEL};
      37 | };
      38 |

      at Object.localStorage [as cancel] (src/actions/AllActions.js:35:3)
      at Object.cancel (__test__/actions/AllActions.test.js:56:26)

Therefore I made a mock for local storage

const mockStorage = {};
const localStorage = {
  setItem: (key, val) => Object.assign(mockStorage, { [key]: val }),
  getItem: key => mockStorage[key],
  removeItem: key => {delete mockStorage[key];console.log("Deleted: ", key);},
  clear: () => mockStorage
};
export default localStorage;

either importing this mock or using localStorage API, I still get same error

I got confused in how should I test that when I call cancel() also localStorge.removeItem is also called or the key and value is removed?

skyboyer
  • 22,209
  • 7
  • 57
  • 64
Amir-Mousavi
  • 4,273
  • 12
  • 70
  • 123

3 Answers3

1

For some reason, there's no window.localStorage object inside Jest's test env. I'd recommend you to assign your mock to the window object. You can do it inside beforeAll method in your test file or globally for all your tests as described here.

After that, your code and tests will use it as native localStorage:

test("user cancels", () => {
  localStorage.setItem("MyAppData", "some_data");

  const action = actions.cancel();
  expect(action).toEqual({
    type: types.USER_CANCEL
  });

  expect(localStorage.getItem("MyAppData")).toBeNull();
});
  • Hi, Thanks, but when do not use the mock I get ReferenceError: localStorage is not defined in test, and when importing mock get same error from action itself – Amir-Mousavi Sep 09 '18 at 21:41
  • It may happen if your test environment is not `jsdom`. Because node.js doesn't have `localStorage`. Maybe [this](https://jestjs.io/docs/en/configuration.html#testenvironment-string) will help. – Oleksandr Kovpashko Sep 09 '18 at 21:48
  • how should I recognize that? I have created with create-react-app CLI and all babel plugins for jest with .babelrc. – Amir-Mousavi Sep 09 '18 at 21:55
  • even if I do not write any test regarding local storage, and just check the return "type" again I get same error – Amir-Mousavi Sep 09 '18 at 22:00
  • Hi, again :) I have edited question with error log, if it helps – Amir-Mousavi Sep 09 '18 at 22:07
  • I think I figured it out. For some reason, there's no `window.localStorage` object inside test env. It's quite strange and I will investigate it tomorrow. For now, I can only suggest you assign your mock to the `window` object before calling your action creator. I will update my answer with and example. – Oleksandr Kovpashko Sep 09 '18 at 22:12
1

If you're mocking localStorage, mock all its methods such as removeItem in order to see if it is invoked, you can use console.log in its mocked method.

Inside jest.config.js

Add

  setupFiles: [
    './jest.globals.js'
  ],

jest.globals.js

const mockStorage = {};
const localStorage = {
  setItem: (key, val) => Object.assign(mockStorage, {[key]: val}),
  getItem: key => mockStorage[key],
  removeItem: key => { delete mockStorage[key]; console.log('Deleted: ' + key); },
  clear: () => mockStorage,
};
global.localStorage = localStorage;

Now localStorage will be available globally throughout your tests.

To test:

test("user cancels", () => {
     const action = actions.cancel();
      expect(action).toEqual({
        type: types.USER_CANCEL
      });

      expect(localStorage.getItem("MyAppData")).toBeNull();
});

Edit:

As a final solution update your jest.config.js to use testEnvironment: "jsdom" this would use the localStorage of jsdom, you no longer will need to mock it.

Ayushya
  • 1,862
  • 1
  • 10
  • 15
  • `console.log` will not fail your test if the `removeItem` method is not called. You should rely only on assertions. – Oleksandr Kovpashko Sep 09 '18 at 20:55
  • It answered on how to test if `localStorge.removeItem` was invoked, updating it for assertions as well. – Ayushya Sep 09 '18 at 21:01
  • But your answer can be understood as a suggestion to test the logic via console.log. Probably replacing "in order to test if it is invoked" with "in order to see if it is invoked" will fix this ambiguity. – Oleksandr Kovpashko Sep 09 '18 at 21:05
  • 1
    Sure, that brings more clarity. – Ayushya Sep 09 '18 at 21:07
  • @Ayushya Thanks, but when I do not use mock I get ReferenceError: localStorage is not defined in the action file itself and when importing mock get the error in test file. where am I going wrong? – Amir-Mousavi Sep 09 '18 at 21:44
  • @Amir-Mousavi You are getting this error since localStorage is not available in global scope, you need to configure jest.config.js, add a file in setupFiles section, and set localStorage globally, updating my original answer – Ayushya Sep 10 '18 at 05:38
  • Thanks, I did so, but now I get-> TypeError: _localStorage2.default.getItem is not a function – Amir-Mousavi Sep 10 '18 at 09:37
  • @Amir-Mousavi Can you show the complete code for the same ? – Ayushya Sep 10 '18 at 11:41
  • @Ayushya yes, Thanks, this is the link https://github.com/AmirHMousavi/battleship-game.git – Amir-Mousavi Sep 10 '18 at 11:50
  • @Amir-Mousavi I had a look on your repository, and found out the solution which will work for you. in your jest.config.js, update testEnvironment to testEnvironment: "jsdom" And remove all the mock for localStorage, with jsdom we'll no longer need mock for localStorage. This passed all tests on my machine. – Ayushya Sep 10 '18 at 14:26
0

Thanks to @Ayushya and @Oleksandr Kovpashko. I just found out if the app is created by CRA the whole approach is much more simpler. so uninstalled all packages and revert back to CRA defaults. and to the best of my knowledge, only one answer was mentioning that. Also I was searching more at jest docs where the answer was in CRA docs

so create src/setupTests.js

const localStorageMock = {
  getItem: jest.fn(),
  setItem: jest.fn(),
  removeItem: jest.fn(),
  clear: jest.fn()
};
global.localStorage = localStorageMock;

by default the test script of CRA is

 "test": "react-scripts test --env=jsdom",

that can be changed to node

Amir-Mousavi
  • 4,273
  • 12
  • 70
  • 123