Mock functions in individual tests using Jest

tylerlwsmith

Tyler Smith

Posted on August 4, 2024

Mock functions in individual tests using Jest

Sometimes you want to mock a function in some tests but not others. Sometimes you want to supply different mocks to different tests. Jest makes this tricky: its default behavior is to override a package's function for a whole test file, not just a single test. This seems odd if you've used flexible tools like Python's @patch or Laravel's service container.

This post will show you how to mock functions for individual tests, then fallback to the original implementation if no mock was provided. Examples will be given for both CommonJS and ES modules. The techniques demonstrated in this post will work for both first-party modules and third-party packages.

CommonJS vs ES Modules

Since we'll be covering multiple module systems in this post, it's important to understand what they are.

CommonJS (abbreviated CJS) is the module system in Node.js. It exports functions using module.exports and imports functions using require():

// CommonJS export 

function greet() {
  return "Hello, world!";
}

module.exports = { greet };
Enter fullscreen mode Exit fullscreen mode
// CommonJS import

const getUsersList = require('./greet');
Enter fullscreen mode Exit fullscreen mode

ES modules (abbreviated ESM) is the module system that's used by the browser. It exports functions using the export keyword and imports functions using the import keyword:

// ES module export

export default function greet() {
  return "Hello, world!";
}
Enter fullscreen mode Exit fullscreen mode
// ES module import

import { greet } from "./greet";
Enter fullscreen mode Exit fullscreen mode

Most frontend JavaScript developers use ES modules at the time of writing this post, and many server-side JS devs use them as well. However, CommonJS is still the default for Node. Regardless of which system you use, it is worth reading the whole article to learn about Jest's mocking system.

Mocking a single exported function with CommonJS

Typically a CommonJS file will export their modules using object syntax, like shown below:

// CommonJS export 

function greet() {
  return "Hello, world!";
}

module.exports = { greet: greet };
Enter fullscreen mode Exit fullscreen mode

However, it is also possible to export a function by itself:

// CommonJS export 

function greet() {
  return "Hello, world!";
}

module.exports = greet;
Enter fullscreen mode Exit fullscreen mode

I wouldn't necessarily recommend doing this in your own code: exporting an object will give you fewer headaches while developing your application. However, it is common enough that it's worth discussing how to mock a bare exported function in CommonJS, then fallback to the original if a test does not provide its own implementation.

Let's say we have the following CommonJS file we'd like to mock during tests:

// cjsFunction.js

function testFunc() {
  return "original";
}

module.exports = testFunc;
Enter fullscreen mode Exit fullscreen mode

We could mock it in our tests using the following code:

const testFunc = require("./cjsFunction");

jest.mock("./cjsFunction");

beforeEach(() => {
  testFunc.mockImplementation(jest.requireActual("./cjsFunction"));
});

it("can override the implementation for a single test", () => {
  testFunc.mockImplementation(() => "mock implementation");

  expect(testFunc()).toBe("mock implementation");
  expect(testFunc.mock.calls).toHaveLength(1);
});

it("can override the return value for a single test", () => {
  testFunc.mockReturnValue("mock return value");

  expect(testFunc()).toBe("mock return value");
  expect(testFunc.mock.calls).toHaveLength(1);
});

it("returns the original implementation when no overrides exist", () => {
  expect(testFunc()).toBe("original");
  expect(testFunc.mock.calls).toHaveLength(1);
});
Enter fullscreen mode Exit fullscreen mode

How it works

When we call jest.mock("./cjsFunction"), this replaces the module (the file and all of its exports) with an auto-mock (docs). When an auto-mock is called, it will return undefined. However, it will provide methods for overriding the mock's implementation, return value, and more. You can see all the properties and methods it provides in the Jest Mock Functions documentation.

We can use the mock's mockImplementation() method to automatically set the mock's implementation to the original module's implementation. Jest provides a jest.requireActual() method that will always load the original module, even if it is currently being mocked.

Mock implementations and return values are automatically cleared after each test, so we can pass a callback function to Jest's beforeEach() function that sets the implementation of the mock to the original implementation before each test. Then any tests that wish to provide their own return value or implementation can do that manually within the test body.

Mocking CommonJS when exporting an object

Let's say that the code above had exported an object instead of a single function:

// cjsModule.js

function testFunc() {
  return "original";
}

module.exports = {
  testFunc: testFunc,
};
Enter fullscreen mode Exit fullscreen mode

Our tests would then look like this:

const cjsModule = require("./cjsModule");

afterEach(() => {
  jest.restoreAllMocks();
});

it("can override the implementation for a single test", () => {
  jest
    .spyOn(cjsModule, "testFunc")
    .mockImplementation(() => "mock implementation");

  expect(cjsModule.testFunc()).toBe("mock implementation");
  expect(cjsModule.testFunc.mock.calls).toHaveLength(1);
});

it("can override the return value for a single test", () => {
  jest.spyOn(cjsModule, "testFunc").mockReturnValue("mock return value");

  expect(cjsModule.testFunc()).toBe("mock return value");
  expect(cjsModule.testFunc.mock.calls).toHaveLength(1);
});

it("returns the original implementation when no overrides exist", () => {
  expect(cjsModule.testFunc()).toBe("original");
});

it("can spy on calls while keeping the original implementation", () => {
  jest.spyOn(cjsModule, "testFunc");

  expect(cjsModule.testFunc()).toBe("original");
  expect(cjsModule.testFunc.mock.calls).toHaveLength(1);
});
Enter fullscreen mode Exit fullscreen mode

How it works

The jest.spyOn() method allows Jest to record calls to a method on an object and provide its own replacement. This only works on objects, and we can use it because our module is exporting an object that contains our function.

The spyOn() method is a mock, so its state must be reset. The Jest spyOn() documentation recommends resetting the state using jest.restoreAllMocks() in an afterEach() callback, which is what we did above. If we did not do this, the mock would return undefined in the next test after spyOn() was called.

Mocking ES modules

ES modules can have default and named exports:

// esmModule.js

export default function () {
  return "original default";
}

export function named() {
  return "original named";
}
Enter fullscreen mode Exit fullscreen mode

Here's what the tests for the file above would look like:

import * as esmModule from "./esmModule";

afterEach(() => {
  jest.restoreAllMocks();
});

it("can override the implementation for a single test", () => {
  jest
    .spyOn(esmModule, "default")
    .mockImplementation(() => "mock implementation default");
  jest
    .spyOn(esmModule, "named")
    .mockImplementation(() => "mock implementation named");

  expect(esmModule.default()).toBe("mock implementation default");
  expect(esmModule.named()).toBe("mock implementation named");

  expect(esmModule.default.mock.calls).toHaveLength(1);
  expect(esmModule.named.mock.calls).toHaveLength(1);
});

it("can override the return value for a single test", () => {
  jest.spyOn(esmModule, "default").mockReturnValue("mock return value default");
  jest.spyOn(esmModule, "named").mockReturnValue("mock return value named");

  expect(esmModule.default()).toBe("mock return value default");
  expect(esmModule.named()).toBe("mock return value named");

  expect(esmModule.default.mock.calls).toHaveLength(1);
  expect(esmModule.named.mock.calls).toHaveLength(1);
});

it("returns the original implementation when no overrides exist", () => {
  expect(esmModule.default()).toBe("original default");
  expect(esmModule.named()).toBe("original named");
});
Enter fullscreen mode Exit fullscreen mode

How it works

This looks almost the same as the previous CommonJS example, with a couple of key differences.

First, we're importing our module as a namespace import.

import * as esmModule from "./esmModule";
Enter fullscreen mode Exit fullscreen mode

Then when we want to spy on the default export, we use "default":

  jest
    .spyOn(esmModule, "default")
    .mockImplementation(() => "mock implementation default");
Enter fullscreen mode Exit fullscreen mode

Troubleshooting ES module imports

Sometimes when trying to call jest.spyOn() with a third-party package, you'll get an error like the one below:

    TypeError: Cannot redefine property: useNavigate
        at Function.defineProperty (<anonymous>)
Enter fullscreen mode Exit fullscreen mode

When you run into this error, you'll need to mock the package that is causing the issue:

import * as reactRouterDOM from "react-router-dom";

// ADD THIS:
jest.mock("react-router-dom", () => {
  const originalModule = jest.requireActual("react-router-dom");

  return {
    __esModule: true,
    ...originalModule,
  };
});

afterEach(() => {
  jest.restoreAllMocks();
});
Enter fullscreen mode Exit fullscreen mode

This code replaces the module with a Jest ES Module mock that contains all of the module's original properties using jest.mocks's factory parameter. The __esModule property is required whenever using a factory parameter in jest.mock to mock an ES module (docs).

If you wanted, you could also replace an individual function in the factory parameter. For example, React Router will throw an error if a consumer calls useNavigate() outside of a Router context, so we could use jest.mock() to replace that function throughout the whole test file if we desired:

jest.mock("react-router-dom", () => {
  const originalModule = jest.requireActual("react-router-dom");

  return {
    __esModule: true,
    ...originalModule,

    // Dummy that does nothing.
    useNavigate() {
      return function navigate(_location) {
        return;
      };
    },
  };
});
Enter fullscreen mode Exit fullscreen mode

Wrapping up

I hope this information is valuable as you write your own tests. Not every app will benefit from being able to fallback to the default implementation when no implementation is provided in a test itself. Indeed, many apps will want to use the same mock for a whole testing file. However, the techniques shown in this post will give you fine-grained control over your mocking, allowing you to mock a function for a single test in Jest.

Let me know if I missed something or if there's something that I didn't include in this post that should be here.

πŸ’– πŸ’ͺ πŸ™… 🚩
tylerlwsmith
Tyler Smith

Posted on August 4, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related