Michael Evans
Michael Evans

Reputation: 1021

Stubbing function with jest

I understand that this is the basic function of jest and I should be able to figure it out with the docs and other online resources, but somehow I cannot, so I apologise in advance if this is trivial.

I'm trying to test a function in Javascript that performs a few operations using other modules, as well as localStorage, and I would like to stub out the other modules and the call to localStorage. The documentation that I found for Jest seemed far too simplistic for me to adapt to my use case, like declaring a mock and then calling it inside the test - this doesn't happen in my case, as the function I want to mock is being called internally by my function, I'm not passing it in as a dependency. Let me give some code to explain: file name is dataInLocalStorage.js

import serialize from './serialize'; // simple module that serialises data
import deserialize from './deserialize'; // simple module that deserialises data
import findObject from './findObject'; // find an object in the deserialised data

const addDataToLocalStorage = (data) => {
  const dataStored = deserialize(localStorage.getItem('data')); // fetch data from localStorage
  const isStored = !!findObject(dataStored, data); // check whether the data I want to store is already there

  if (isStored) { return null; } // if data is already stored, skip

  const serializedData = serialize(data); // serialise data to be stored

  return localStorage.setItem('data', serializedData); // store serialised data in localStorage
};

export { addDataToLocalStorage };

The purpose os this module is just to store data in localStorage in a serialised way, but in an additive way, so that adding data doesn't remove previously stored data, and no duplicates are added either.

Anyway, my test file looks like this: file name is dataInLocalStorage.test.js

import { addDataToLocalStorage } from '../dataInLocalStorage';

describe('addDataToLocalStorage', () => {
  const deserialize = jest.fn();

  beforeAll(() => {
    localStorage.removeItem('data');
  });

  const data = {
    name: 'johnny'
  };

  addDataToLocalStorage(data);

  it('adds the data to local storage', () => {
    expect(deserialize).toHaveBeenCalled();
  });
});

Here is the rather unsurprising error for this attempt.

expect(jest.fn()).toHaveBeenCalled()

Expected mock function to have been called, but it was not called.

      17 |
      18 |   it('adds the data to local storage', () => {
    > 19 |     expect(deserialize).toHaveBeenCalled();
         |                         ^
      20 |   });
      21 | });

On top of this I tried importing the deserialize function here in the test file and adding a jest.mock on that, which didn't work either.

Note that this isn't my code 100%, I have modified it for simplicity in order to make it easier to read for you, sorry if there are some slight mismatches, I tried my best to be as diligent as possible while converting it.

If you know what you're looking at, you'll see that this is obviously not working. Using other (more useful) expectations, the test was passing, but adding some console logs in the deserialize file showed that it's still running, when the idea is that I would like to mock it and provide my own return value.


Side note: I came from Ruby on Rails where mocking with RSpec is pretty simple, and I was hoping it would be just as simple with Jest. It likely is, but I can't wrap my head around it, as it doesn't seem possible to make a direct reference to the function/module I want to mock. In RSpec, doing allow(MySerializer).to receive(:call).and_return(...) would do the trick and I wouldn't have to worry about that module being called during the test.

Upvotes: 0

Views: 706

Answers (1)

lecstor
lecstor

Reputation: 5707

When you set the value of deserialize to a jest mock, you are changing the variable value, not setting a reference that your code is using. To keep it a reference it needs to be a value in an object.

To import an object you can use import * as deserialize from "./deserialize";. Then you can set the mock on the reference with deserialize.default = jest.fn().

https://codesandbox.io/s/88wlzp6q88

import { useIt } from "./use-default-export";

import * as myfunc from "./default-export-function";

test("use-default-export-function", () => {
  expect(useIt()).toEqual("real");
});

test("use-default-export-function with mock", () => {
  myfunc.default = jest.fn(() => "unreal");
  expect(useIt()).toEqual("unreal");
});

in your test it'll be..

import { addDataToLocalStorage } from '../dataInLocalStorage';
import * as deserialize from './deserialize';
...
deserialize.default = jest.fn();

alternate TS compat version.. (which is actually cleaner all round..)

import { useIt } from "./use-default-export";

import myfunc from "./default-export-function";

jest.mock("./default-export-function", () => jest.fn());

test("use-default-export-function with mock", () => {
  useIt();
  expect(myfunc).toHaveBeenCalled();
});

return/resolve different values per test (need to cast to jest.Mock to be able to use jest.fn() functions)

test("use-default-export-function with mock", () => {
  const aFunc = myfunc as jest.Mock;
  aFunc.mockResolvedValue("bar");
  useIt();
  expect(useIt()).resolves.toEqual("bar");
});

test("use-default-export-function with mock 2", () => {
  const aFunc = myfunc as jest.Mock;
  aFunc.mockReturnValue("foo");
  useIt();
  expect(useIt()).toEqual("foo");
});

Upvotes: 1

Related Questions