Change Jest mock per test

Changing implementation of Jest mocks per test can be confusing. This blog post will present a simple solution for that. You’ll see how each test can get its own mock for both constant values and functions. The solution doesn’t rely on using require().

Sample app

Let’s start with an example - we have a function - sayHello(name) - it prints out Hi, ${name}. And depending on configuration it either capitalizes the name or not.

say-hello.js

import { CAPITALIZE } from "./config";

export const sayHello = (name) => {
  let result = "Hi, ";

  if (CAPITALIZE) {
    result += name[0].toUpperCase() + name.substring(1, name.length);
  } else {
    result += name;
  }

  return result;
};

And we want to test its behaviour like this:

say-hello.js

import { sayHello } from "./say-hello";

describe("say-hello", () => {
  test("Capitalizes name if config requires that", () => {
    expect(sayHello("john")).toBe("Hi, John");
  });

  test("does not capitalize name if config does not require that", () => {
    expect(sayHello("john")).toBe("Hi, john");
  });
});

One of those tests is bound to fail. Which one - depends on the value of CAPITALIZE.

Setting a value inside jest.mock() will not help either. It will be the same as relying on the hardcoded value - one of the tests will fail.

jest.mock("./config", () => ({
  CAPITALIZE: true, // or false
}));

Changing mock of non-default const

So we need to change the mock of a non-default const.

First, let’s change the way we mock the config module:

jest.mock("./config", () => ({
  __esModule: true,
  CAPITALIZE: null,
}));

We do set CAPITALIZE to null, because we’ll set its real value in the individual tests. We also have to specify __esModule: true, so that we could correctly import the entire module with import * as config.

Next step is we need to import the module:

import * as config from "./config";

And finally change the mock value in each test:

import { sayHello } from "./say-hello";
import * as config from "./config";

jest.mock("./config", () => ({
  __esModule: true,
  CAPITALIZE: null,
}));

describe("say-hello", () => {
  test("Capitalizes name if config requires that", () => {
    config.CAPITALIZE = true;

    expect(sayHello("john")).toBe("Hi, John");
  });

  test("does not capitalize name if config does not require that", () => {
    config.CAPITALIZE = false;

    expect(sayHello("john")).toBe("Hi, john");
  });
});

How does it work?

jest.mock() replaces the entire module with a factory function we provide in its second argument. So when we import that module we get a mock instead of the real module. That also means that we can import the same module in the test itself. And that will give us access to the mock which behaviour we can change.

Why import entire module versus just the const we need?

Why can’t we just import in this way import CAPITALIZE from './config';? If we import it in that way, we won’t be able to re-assign a value to it. Values are always imported as constants.

TypeScript

If you’re using TypeScript the line where you’re changing the mock:

config.CAPITALIZE = true;

will give you an error:

Cannot assign to 'CAPITALIZE' because it is a read-only property

That’s because TypeScript treats imports as constants and objects with read-only properties.

We can fix that by type casting to an object with writeable properties, e.g.:

import * as config from "./config";

const mockConfig = config as { CAPITALIZE: boolean };

// and then in a test
mockConfig.CAPITALIZE = true;

Changing mock of export default const

Okay, but what if we need to change the mock of a value that is a default export of the module?

const CAPITALIZE = true;

export default CAPITALIZE;

We can use the same approach, we just need to mock the default attribute:

import { sayHello } from "./say-hello";
import * as config from "./config";

jest.mock("./config", () => ({
  __esModule: true,
  default: null,
}));

describe("say-hello", () => {
  test("Capitalizes name if config requires that", () => {
    config.default = true;

    expect(sayHello("john")).toBe("Hi, John");
  });

  test("does not capitalize name if config does not require that", () => {
    config.default = false;

    expect(sayHello("john")).toBe("Hi, john");
  });
});

TypeScript

As with mocking a constant that is non-default export, we need to type cast the imported module into an object with writeable properties

We can fix that by type casting to an object with writeable properties. This time though we change the default attribute instead of CAPITALIZE.

import * as config from "./config";

const mockConfig = config as { default: boolean };

// and then in a test
mockConfig.default = true;

Changing mock of non-default function

What if the configuration is returned by a function instead of a constant:

const CAPITALIZE = true;

export default CAPITALIZE;

Actually, it’ll be even more straightforward than dealing with constants, as we don’t need to import the entire module via import * as entireModule and as a result we won’t have to provide __esModule: true.

Our test will simply looks like this:

import { sayHello } from "./say-hello";
import { shouldCapitalize } from "./config";

jest.mock("./config", () => ({
  shouldCapitalize: jest.fn(),
}));

describe("say-hello", () => {
  test("Capitalizes name if config requires that", () => {
    shouldCapitalize.mockReturnValue(true);

    expect(sayHello("john")).toBe("Hi, John");
  });

  test("does not capitalize name if config does not require that", () => {
    shouldCapitalize.mockReturnValue(false);

    expect(sayHello("john")).toBe("Hi, john");
  });
});

TypeScript

This line

shouldCapitalize.mockReturnValue(false);

will give a TypeScript error of:

Property 'mockReturnValue' does not exist on type '() => boolean'.

Indeed, TypeScript thinks we’ve imported a function that returns a boolean, not a Jest mock.

We can correct it again with type casting to a Jest mock.

import { shouldCapitalize } from "./config";

const mockShouldCapitalize = shouldCapitalize as jest.Mock;

// and then in a test
mockConfig.default = true;

Changing mock of default function

There might also be a case that we want to change the behaviour of the function that is the default export of a module.

const shouldCapitalize = () => true;

export default shouldCapitalize;

In that case, we employ a technique similar mocking default constants - we'll mock default, set __esModule: true and will import the entire module with *.

import { sayHello } from "./say-hello";
import * as config from "./config";

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

describe("say-hello", () => {
  test("Capitalizes name if config requires that", () => {
    config.default.mockReturnValue(true);
    expect(sayHello("john")).toBe("Hi, John");
  });

  test("does not capitalize name if config does not require that", () => {
    config.default.mockReturnValue(false);
    expect(sayHello("john")).toBe("Hi, john");
  });
});

TypeScript

Similar to mocking a non default function, we need to type cast the imported module into an object with writeable properties

import * as config from "./config";

const shouldCapitalizeMock = config.default as jest.Mock;

// and in a test
shouldCapitalizeMock.mockReturnValue(true);

Conclusion

All examples above rely on a simple premise that:

  • jest.mock() mocks a specific module (unsurprisingly, huh?)
  • So everywhere you import it you’ll get a mock instead of a real module
  • And that applies to tests, as well
  • So import mocked modules in test and change their implementation
Mike Borozdin (Twitter)
20 February 2021

The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way. My personal thoughts tend to change, hence the articles in this blog might not provide an accurate reflection of my present standpoint.

© Mike Borozdin