DEV Community

Cover image for Jest mocking strategies
Mercedes Bernard
Mercedes Bernard

Posted on • Originally published at mercedesbernard.com

Jest mocking strategies

This post was last updated on July 12, 2020. Always refer to library documentation for the most updated information.

Note: this post assumes familiarity with Jest and mocking. If you want to learn more, take a look at the Jest docs first 🙂

Table of contents

  1. ES6 Exports
  2. Mock behavior
  3. Common mocking errors

Recently, I've been spending more time wrestling with uncooperative mocks than writing the code or the tests combined. I created this post to serve as an easily navigable guidebook of strategies for the next time jest.mock('modulename') won't cut it. This is not an exhaustive list, there are multiple ways to satisfy every use case.

When mocking a module or function, there are 2 main things to consider:

  1. How was the module exported?
  2. What do we want the behavior of our mock to be?

All of the following code samples can be found on my Github. The sample application shows you a random cute image of an animal every 3 seconds. It's a React app with Jest as the test runner and uses React Testing Library to test the component DOM (this is the default configuration with Create React App).

Despite being built with React, the mocking examples should be easily portable to any framework.

ES6 Exports

Before we worry about our mock's behavior, it's important to understand how the package we're using is exported.

When publishing a package, the maintainer makes decisions such as choosing default or named exports and whether to export a vanilla function, an object, or a function that returns an object of other functions. All of those choices affect how the package needs to be mocked in the tests of our application code.

Below, we'll look at some tiny examples to highlight how different exports change our mock strategy.

Default export of a vanilla function

In our first example, the library exports is a single default function. When this function is called, it executes the library's logic.

export default function () {
  return "real value";
}

To mock its implementation, we use a default import, mock the module, and provide a factory (a function which will run when the module is invoked).

Because the module is a function, we provide a factory that returns the mock function we want to be invoked instead of the module. In this example, we provided a mock implementation so we could set the return value.

import example from "../defaultFunction";

const mockExpected = "mock value";
jest.mock("../defaultFunction", () => jest.fn(() => mockExpected));

it("returns the expected value", () => {
  const actual = example();
  expect(actual).toEqual(mockExpected);
});

Named export of a vanilla function

In our first example, the library exports is a single named function. When this function is called, it executes the library's logic.

export const example = () => {
  return "real value";
};

To mock its implementation, we use a named import, mock the module, and provide a factory that returns an object with the named function and its mock implementation.

This is slightly different than the previous example because of the named export.

import { example } from "../namedFunction";

const mockExpected = "mock value";
jest.mock("../namedFunction", () => ({
  example: jest.fn(() => mockExpected),
}));

it("returns the expected value", () => {
  const actual = example();
  expect(actual).toEqual(mockExpected);
});

Default export of an object

In this example, the library exports a default object that has a property for the function we want to mock.

export default {
  getValue: () => "real value",
};

To mock getValue, we use a default import, spy on the imported object's getValue property, and then chain a mock implementation to the returned mock function.

Because example is an object, we can spy on its properties. If we didn't want to mock the implementation, we could leave that part off and still be able to track that the returned mock function was called.

* Note: jest.spyOn invokes the function's original implementation which is useful for tracking that something expected happened without changing its behavior. For true mocking, we use mockImplementation to provide the mock function to overwrite the original implementation.

import example from "../defaultObject";

const mockExpected = "mock value";
jest.spyOn(example, "getValue").mockImplementation(jest.fn(() => mockExpected));

it("returns the expected value", () => {
  const actual = example.getValue();
  expect(actual).toEqual(mockExpected);
});

Named export of an object

In this example, the library exports a named object that has a property for the function we want to mock.

export const example = {
  getValue: () => "real value",
};

Mocking getValue on the named export is the same as mocking it on the default export 🥳 This is one of the few cases where the export type doesn't matter because it's an object that can be spied on.

import { example } from "../namedObject";

const mockExpected = "mock value";
jest.spyOn(example, "getValue").mockImplementation(jest.fn(() => mockExpected));

it("returns the expected value", () => {
  const actual = example.getValue();
  expect(actual).toEqual(mockExpected);
});

Default export of a function that returns an object

This example is a bit more complicated than the previous ones. Here, the library exports a default function that returns an object that has a property for the function that we want to mock. This is a common pattern to allow developers to destructure their desired function off the module function.

const { getValue } = example()

As a simple example, it looks like this.

export default function () {
  return {
    getValue: () => "real value",
  };
}

To mock getValue, we use a default import to import the entire module's contents (the * as syntax which allows us to treat the module name as a namespace), spy on the imported module's default property, and then chain a mock implementation to the returned mock function.

In this case, our mock implementation is a function that returns an object with a getValue property. getValue is a mock function.

import * as exampleModule from "../defaultFunctionReturnObject";

const mockExpected = "mock value";
jest.spyOn(exampleModule, "default").mockImplementation(() => ({
  getValue: jest.fn(() => mockExpected),
}));

it("returns the expected value", () => {
  const { getValue } = exampleModule.default();
  const actual = getValue();
  expect(actual).toEqual(mockExpected);
});

Named export of a function that returns an object

Similar to the previous example, the library exports a named function that returns an object that has a property for the function that we want to mock.

export function example() {
  return {
    getValue: () => "real value",
  };
}

Mocking this use case is very similar to the default export case above except that we need to spy on the named export rather than the default export.

To mock getValue, we use a default import to import the entire module's contents, spy on the imported module's example property (this is the named export), and then chain a mock implementation to the returned mock function.

In this case, our mock implementation is a function that returns an object with a getValue property, just like in our previous example.

import * as exampleModule from "../namedFunctionReturnObject";

const mockExpected = "mock value";
jest.spyOn(exampleModule, "example").mockImplementation(() => ({
  getValue: jest.fn(() => mockExpected),
}));

it("returns the expected value", () => {
  const { getValue } = exampleModule.example();
  const actual = getValue();
  expect(actual).toEqual(mockExpected);
});

Mock behavior

We've seen how different export strategies affect how we structure our mocks. Let's look next at how to change our mocks based on the desired behavior we want within our tests.

Browser functionality

For the whole test suite

If we are using a browser API throughout our application, we may want to mock it for your entire test suite. I reach for this strategy often for localStorage and sessionStorage.

For example, here's a mock implementation of sessionStorage.

export class SessionStorageMock {
  constructor() {
    this.store = {};
  }

  clear() {
    this.store = {};
  }

  getItem(key) {
    return this.store[key] || null;
  }

  setItem(key, value) {
    this.store[key] = value.toString();
  }

  removeItem(key) {
    delete this.store[key];
  }
}

And then in the setup file, we'll reset the global sessionStorage implementation to our mock implementation for the duration of the test suite.

const unmockedSessionStorage = global.sessionStorage;

beforeAll(() => {
  global.sessionStorage = new SessionStorageMock();
});

afterAll(() => {
  global.sessionStorage = unmockedSessionStorage;
});

While the tests run, any code that inserts/removes from sessionStorage will use our mock implementation and then we can assert on it in the test files.

it("sets sessionStorage isFetching to true", () => {
  const { getByText } = render(subject);
  const button = getByText(
    new RegExp(`please fetch me some cute ${animal}`, "i")
  );
  act(() => {
    fireEvent.click(button);
  });
  expect(sessionStorage.getItem("isFetching")).toEqual("true");
});

In a single file

If we're using a browser API, but want different behavior throughout our tests we may choose to mock it in the relevant test files.

This is helpful when we're using the browser fetch API and want to mock different responses in our tests. We can use a beforeEach block to set our global.fetch mock implementation.

We set global.fetch to a mock function and use Jest's mockResolvedValue (syntactic sugar wrapping mockImplementation) to return a mock response in the shape our code expects.

beforeEach(() => {
  jest.resetAllMocks();
  global.fetch = jest.fn().mockResolvedValue({
    status: 200,
    ok: true,
    json: () => Promise.resolve({ media: { poster: "hello" } }),
  });
});

Then we can assert that global.fetch was called the expected number of times.

 it("fetches an image on initial render", async () => {
  jest.useFakeTimers();
  render(subject);
  await waitFor(() => expect(global.fetch).toHaveBeenCalledTimes(1));
});

Node modules

For the whole test suite

Sometimes we are using a node module throughout our code and we want to mock it for our entire test suite. In this case, we can create a manual mock that Jest will automatically use during tests whenever it encounters references to that module.

In this little sample application, we're using Voca to capitalize some words in our navigation. To create a manual mock, we create a folder named __mocks__ inside of our src directory and place our mock implementation there. Note: this is counter to what the documentation says. At the time of writing, there is an open issue documenting this. The fix appears to be placing your mocks inside src instead of adjacent to node_modules.

In our mock, we use jest.genMockFromModule (or jest.createMockFromModule) to create an automock and then extend it with our mock implementation for the relevant function(s). By extending an automock, you limit how often you have to manually update your manual mock when the original module changes.

const voca = jest.genMockFromModule("voca");
voca.capitalize = (word) => `${word} capitalize mocked!`;
export default voca;

Then you can assert on the expected behavior of your mock within your tests.

it("capitalizes the current page name", () => {
  const { getByText } = render(subject);
  expect(getByText(/capitalize mocked!/i)).toBeInTheDocument();
});

In a single file

Mocking an entire node module for a single file in our test suite is not that different than what we did to mock it for the whole suite. Instead of placing our code in our setup file, we put it in the test file where we want the mocking to occur.

To mock moment in one test file, we can do something very similar to what we did for pluralize. We use a default import, mock the module, and make sure that the default return shape matches the return shape of the original implementation.

Assuming the code we want to test looks like this

export const toMoment = (datetime) => {
  return moment(datetime);
};

We would mock moment like this

import moment from "moment";

jest.mock("moment", () => ({
  __esModule: true,
  default: jest.fn(),
}));

Then we can assert that our mock moment function was called

describe("toMoment", () => {
  it("calls moment() with the correct params", () => {
    const dateParam = new Date();
    toMoment(dateParam);
    expect(moment).toHaveBeenCalledWith(dateParam);
  });
});

If we want to use some of the functions returned from Moment's default function, we need to update our mock to have mock implementations for those as well.

let mockFormat = jest.fn();
jest.mock("moment", () => ({
  __esModule: true,
  default: jest.fn(() => ({ format: mockFormat })),
}));

A single function of a node module

For the whole test suite

Just like how we may want to mock browser functionality for our entire test suite, sometimes we may want to mock a node module for our test suite instead of in individual files.

In this case, we can mock it in our setup file so that all tests in the suite use that mock. In our sample application, we mock the Pluralize module for all of our tests.

In our setupTests.js file, we mock the default export.

jest.mock("pluralize", () => ({
  __esModule: true,
  default: jest.fn((word) => word),
}));

You'll note that we have __esModule: true here. From Jest's documentation, "When using the factory parameter for an ES6 module with a default export, the __esModule: true property needs to be specified. This property is normally generated by Babel / TypeScript, but here it needs to be set manually."

In a single file

In my experience, the most common mocking use case is to mock the same behavior of one function in a node module for every test in a file. To do this, we declare mock once within the file (remembering what we know about module exports).

For example, in our sample application, we use axios.get to fetch cute pictures of dogs, cats, and foxes. When we're fetching pictures, we want to make sure our code is correctly calling axios.get. And when we're not fetching, we want to make sure we're not making unnecessary requests.

To mock axios.get, we use a default import, spy on the imported object's get property, and then chain a mock implementation to the returned mock function.

import axios from "axios";

jest
  .spyOn(axios, "get")
  .mockImplementation(() => Promise.resolve({ data: { file: "hello" } }));

And then we can assert that axios.get was called the expected number of times.

it("gets a new image on the configured interval", async () => {
  jest.useFakeTimers();
  render(subject);
  await waitFor(() => expect(axios.get).toHaveBeenCalledTimes(1));
  act(() => jest.advanceTimersByTime(refreshTime));
  await waitFor(() => expect(axios.get).toHaveBeenCalledTimes(2));
});

We can also use Jest's syntactic sugar functions to be even terser in our mocking code. The following two examples do the same thing as the mock implementation above.

jest
  .spyOn(axios, "get")
  .mockReturnValue(Promise.resolve({ data: { file: "hello" } }));

And even shorter

jest.spyOn(axios, "get").mockResolvedValue({ data: { file: "hello" } });

In a single test

Finally, sometimes we want to test different behavior within a single test file. We may have error handling or loading states that we want to mock and test that our code behaves appropriately.

In this case, we mock the function that we want with Jest's default mock, jest.fn(), and then we chain a mock implementation on it inside each of our test cases. I like to put the mock implementation in a beforeEach just inside a describe labeled with the case I'm testing, but you can also put it inside an individual test.

In our sample application code, we mock React Router's useParams hook. In our example, we're using Jest's requireActual to make sure we're only mocking the useParams function and nothing else in the module.

import { useParams } from "react-router-dom";

jest.mock("react-router-dom", () => ({
  ...jest.requireActual("react-router-dom"), // use actual everything else
  useParams: jest.fn(),
}));

And then we can set up our different use cases and assert the expected behavior.

describe("with a supported animal type", () => {
  beforeEach(() => {
    useParams.mockReturnValue({
      animal: mockAnimal,
    });
  });

  it("renders the correct animal component(s)", () => {
    const { getAllByText } = render(subject);
    expect(getAllByText(new RegExp(mockAnimal, "i")).length).toBeGreaterThan(
      0
    );
  });
});

describe("without a supported animal type", () => {
  beforeEach(() => {
    useParams.mockReturnValue({
      animal: "hedgehog",
    });
  });

  it("does not render an animal component", () => {
    const { getByText } = render(subject);
    expect(getByText(/oh no/i)).toBeTruthy();
  });
});

Common mocking errors

I find myself running into similar errors over and over when I'm writing tests. I'm sharing fixes I've found in case it's helpful.

The module factory of jest.mock() is not allowed to reference any out-of-scope variables

You'll see this error when you try to use variables that Jest thinks might be uninitialized. The easiest fix is to prefix "mock" to your variable name.

Not allowed

let format = jest.fn();
jest.mock("moment", () => ({
  __esModule: true,
  default: jest.fn(() => ({ format: format })),
}));

Allowed

let mockFormat = jest.fn();
jest.mock("moment", () => ({
  __esModule: true,
  default: jest.fn(() => ({ format: mockFormat })),
}));

Cannot spy the default property because it is not a function

You'll see this error if the object does not have a function for the property you are spying. This usually means that you're not structuring your mock properly and the module is exported differently than what you're configuring. Check out the ES6 Exports examples above to see the various ways you may need to change your spy.

Cannot set property of #<Object> which has only a getter

This error comes up when trying to mock the implementation for an object which has only getters. Unfortunately, I haven't found a way around this other than completely changing my mocking strategy. I run into this most often with React Router.

Spy on default export raises this error

import ReactRouterDom from "react-router-dom";
jest.spyOn(ReactRouterDom, "useParams").mockImplementation(jest.fn());

Spy on the module contents raises "property is not a function" error

import * as ReactRouterDom from "react-router-dom";
jest.spyOn(ReactRouterDom, "default").mockImplementation(() => ({
  useParams: jest.fn(),
}));

Mocking the module, requiring actual and then overwriting the useParams implementation with a mock function works.

jest.mock("react-router-dom", () => ({
  ...jest.requireActual("react-router-dom"), // use actual for all non-hook parts
  useParams: jest.fn(),
}));

Warning: An update inside a test was not wrapped in act

This is not a mocking error specifically, but one that catches me all the time.

If you're seeing this warning but you know that all of your code is wrapped in act(), you might be asserting on promises that haven't resolved yet. React Testing Library has a handy little async utility, waitFor, for this exact use case.

This test raises the "not wrapped in act" warning

it("fetches an image on initial render", async () => {
  jest.useFakeTimers();
  render(subject);
  expect(axios.get).toHaveBeenCalledTimes(1);
});

Wrapping the assertion in waitFor resolves the warning.

it("fetches an image on initial render", async () => {
  jest.useFakeTimers();
  render(subject);
  await waitFor(() => expect(axios.get).toHaveBeenCalledTimes(1));
});

Latest comments (0)