DEV Community

Cover image for How to mock imported functions with Jest
Domagoj Štrekelj
Domagoj Štrekelj

Posted on

How to mock imported functions with Jest

📣 Notice
This article is part of a series:

Testing JavaScript with Jest

To get the most out of this article, I recommend reading the previous article in the series:

How to write unit tests in JavaScript with Jest


Writing unit tests for code with dependencies can be difficult. This is especially true for large code bases where it's tricky to come up with fixtures that will cover all the cases we need to test.

But what if we could control the return value of a function dependency, no matter what arguments it is called with?

This is where mock functions come in.

Mock functions are a testing tool that allows us to track how function dependencies are called and control their return values. This makes it possible for us to manipulate the control flow of the tested program and reach even those difficult-to-reproduce edge-cases when writing tests.

This article will provide an introduction into the concepts behind mocking and how it relates to unit testing. We will learn how to mock functions and imported function modules with Jest, and write tests that rely on those mocks to increase the coverage of our test cases.

We will assume that we're testing a couple of validation rules:

// isInteger.js
module.exports = (value) => Number.isSafeInteger(value);
Enter fullscreen mode Exit fullscreen mode
// isAtLeast18.js
const isInteger = require("./isInteger");

module.exports = (value) => isInteger(value) && value >= 18;
Enter fullscreen mode Exit fullscreen mode

We want to see what our tests will teach us about the flaws in our code by passing and failing test cases. Fixing the implementation is not covered by this article, but feel free to play with it as we move through the article.

Read on to find out more!


How to mock an imported function with Jest?

To mock an imported function with Jest we use the jest.mock() function.

jest.mock() is called with one required argument - the import path of the module we're mocking. It can also be called with an optional second argument - the factory function for the mock. If the factory function is not provided, Jest will automock the imported module.

💡Note
Jest automock is the automatic mocking of imported modules with surface-level replacement implementations. Automocking is disabled by default since Jest 15, but can be enabled by configuring Jest with the automock flag.

When testing isAtLeast18() we have to keep in mind that the isInteger() dependency affects the module's behaviour:

  1. if isInteger() is false , isAtLeast18() is false;
  2. if isInteger() is true , isAtLeast18() depends on the value argument.

We'll start by testing the case of isInteger() returning false.

The isInteger.js module has a single default export - the isInteger() function. We will mock the imported module with a factory function that behaves just like the default export and returns a function. That function, when called, will always return false.

// isAtLeast18.spec.js
const isAtLeast18 = require("./isAtLeast18");

// The mock factory returns the function () => false
jest.mock("./isInteger", () => () => false);

describe("isAtLeast18", () => {
    it("fails if value is not recognised as integer", () => {
        // Should pass, but fails because of the isInteger() mock
        expect(isAtLeast18(123)).toBe(false);
        // Should fail either way
        expect(isAtLeast18("abc")).toBe(false);
    });
});
Enter fullscreen mode Exit fullscreen mode

💡Note
The import path of the mocked module must match the import path that is present in the module we're testing. The isAtLeast18.js module imports the isInteger.js module under the path "./isInteger". This is why our mock import path is also "./isInteger".

isAtLeast18() will now always return false no matter what we call it with, because the isInteger() mock is set to always return false.

But what about the case when isInteger() returns true?

To mock different return values depending on the test we will create a mock function.

💡Note
This unit test is a solitary unit test because the tested unit is isolated from its dependencies. Read more about solitary unit tests in the previous article: How to write unit tests in JavaScript with Jest.


What is a mock function?

A mock function is a function that replaces the actual implementation of a function with a "fake" (mock) implementation.

Mock functions track how they are called by external code. With a mock function we can know the number of times the function was called, the arguments it was called with, the result it returned, and more. This ability to "spy" on function calls is why mock functions are also called spies.

We use mock functions to override original function behaviour with custom mock implementations. Mock implementations help us control the return values of a function. This makes our tests more predictable (deterministic) and easier to write.


How to mock a function with Jest?

To mock a function with Jest we use the jest.fn() function.

jest.fn() can be called with an implementation function as an optional argument. If an implementation is provided, calling the mock function will call the implementation and return it's return value.

If no implementation is provided, calling the mock returns undefined because the return value is not defined.

// Without implementation, this mock returns `undefined`.
const mockUndefined = jest.fn();

// With implementation, this mock returns `true`.
const mockTrue = jest.fn(() => true).
Enter fullscreen mode Exit fullscreen mode

Jest registers mock functions under the "jest.fn()" name by default. We can give the mock function a custom name with the mockName() method. The mock name is used when printing test results.

const mockOne = jest.fn(() => false);
// Example error: expect(jest.fn()).toHaveBeenCalledWith(...expected)

const mockTwo = jest.fn(() => false).mockName('mockTwo');
// Example error: expect(mockTwo).toHaveBeenCalledWith(...expected)
Enter fullscreen mode Exit fullscreen mode

💡Note
It's good practice to name mocked functions in cases where a lot of different mocks are used. This makes it easier to tell mocked functions apart and debug code that isn't matching expectations.


How to change the mock implementation of a function with Jest?

To change the mock implementation of a function with Jest we use the mockImplementation() method of the mocked function.

The mockImplementation() method is called with the new implementation as its argument. The new implementation will then be used in place of the previous one when the mock is called.

// The initial mock is a function that returns `true`.
const myMock = jest.fn(() => true);

// The new mock implementation has the function return `false`.
myMock.mockImplementation(() => false);
Enter fullscreen mode Exit fullscreen mode

We can combine this with jest.mock() factory functions to create mocked modules that contain mocked functions. This way we can control how the implementation of a mock behaves depending on what we're testing.

// isAtLeast18.spec.js
const isAtLeast18 = require("./isAtLeast18");
const isInteger = require("./isInteger");

// The mock factory returns a mocked function
jest.mock("./isInteger", () => jest.fn());

describe("isAtLeast18", () => {
    it("fails if value is not recognised as integer", () => {
        // For this test we'll mock isInteger to return `false`
        isInteger.mockImplementation(() => false);

        expect(isAtLeast18(123)).toBe(false);
        expect(isAtLeast18("abc")).toBe(false);
    });

    it("passes if value is recognised as integer and is at least 18", () => {
        // For this test we'll mock isInteger to return `true`
        isInteger.mockImplementation(() => true);

        expect(isAtLeast18(123)).toBe(true);
        expect(isAtLeast18("abc")).toBe(false);
    });
});
Enter fullscreen mode Exit fullscreen mode

💡Note
jest.mock() works by modifying the Node module cache to give us the mock instead of the original implementation whenever we import a mocked module in a test file. To support ES module imports - where import statements have to come first in a file - Jest automatically hoists jest.mock() calls to the top of the module. Read more about this technique here.


How to check if a function was called correctly with Jest?

To check if a function was called correctly with Jest we use the expect() function with specific matcher methods to create an assertion.

We can use the toHaveBeenCalledWith() matcher method to assert the arguments the mocked function has been called with.

To assert how many times the mocked function has been called so far, we can use the toHaveBeenCalledTimes() matcher method.

// isAtLeast18.spec.js
const isAtLeast18 = require("./isAtLeast18");
const isInteger = require("./isInteger");

jest.mock("./isInteger", () => jest.fn());

describe("isAtLeast18", () => {
  it("fails if value is not recognised as integer", () => {
    isInteger.mockImplementation(() => false);

    expect(isAtLeast18(123)).toBe(false);

        // We expect isInteger to be called with 123
    expect(isInteger).toHaveBeenCalledWith(123);
        // We expect isInteger to be called once
    expect(isInteger).toHaveBeenCalledTimes(1);
  });
});
Enter fullscreen mode Exit fullscreen mode

💡Note
While these are the most common matcher methods for functions, there are more matcher methods available in the Jest API docs.

Jest tracks all calls to mocked functions. A mocked function will remember the arguments and times it has been called, as well as the results of those calls.

When reusing mocked functions between tests it is useful to reset their states before running new tests to get a clear baseline. We can do that by clearing mocked functions between tests.


How to clear mocked functions with Jest?

To clear mocked functions with Jest we use the mockClear() method of a mocked function.

mockClear() resets all information stored in mocked functions which makes it useful for cleaning up a mock's usage data between assertions or tests.

// isAtLeast18.spec.js
const isAtLeast18 = require("./isAtLeast18");
const isInteger = require("./isInteger");

jest.mock("./isInteger", () => jest.fn());

describe("isAtLeast18", () => {
  it("fails if value is not recognised as integer", () => {
    isInteger.mockImplementation(() => false);

    expect(isAtLeast18(123)).toBe(false);
    expect(isInteger).toHaveBeenCalledWith(123);
    expect(isInteger).toHaveBeenCalledTimes(1);

        // Clear the mock so the next test starts with fresh data
    isInteger.mockClear();
  });

  it("passes if value is recognised as integer and is at least 18", () => {
    isInteger.mockImplementation(() => true);

    expect(isAtLeast18(123)).toBe(true);
    expect(isInteger).toHaveBeenCalledWith(123);
        // Without clearing, there would be 2 calls total at this point
    expect(isInteger).toHaveBeenCalledTimes(1);
  });
});
Enter fullscreen mode Exit fullscreen mode

💡Note
Jest also provides mock function methods for resetting and restoring mocked functions, as well as shorthands for creating mocked functions that directly return, resolve, or reject a value.


How to clear mocked functions before each test with Jest?

To clear mocked functions before each test with Jest we use the beforeEach() function.

beforeEach() is called with one required argument - the function to run before each of the tests in the test file. We use it to clear mocks, set up fixtures, or reset some other state used across tests.

// isAtLeast18.spec.js
const isAtLeast18 = require("./isAtLeast18");
const isInteger = require("./isInteger");

jest.mock("./isInteger", () => jest.fn());

// Clear mock data before each test
beforeEach(() => {
  isInteger.mockClear();
});

describe("isAtLeast18", () => {
  it("fails if value is not recognised as integer", () => {
    isInteger.mockImplementation(() => false);

    expect(isAtLeast18(123)).toBe(false);
    expect(isInteger).toHaveBeenCalledWith(123);
    expect(isInteger).toHaveBeenCalledTimes(1);
  });

  it("passes if value is recognised as integer and is at least 18", () => {
    isInteger.mockImplementation(() => true);

    expect(isAtLeast18(123)).toBe(true);
    expect(isInteger).toHaveBeenCalledWith(123);
    expect(isInteger).toHaveBeenCalledTimes(1);
  });
});
Enter fullscreen mode Exit fullscreen mode

💡Note
Jest provides four functions to hook into the set-up and tear-down process, both before and after each or all of the tests in a test file. These functions are: afterAll(), afterEach(), beforeAll(), beforeEach(). The afterAll() and beforeAll() variants are called only once for the entire test file. The afterEach() and beforeEach() variants are called once for every test in the test file.


How to reuse mocks with Jest?

To reuse mocks with Jest we create mocks in a __mocks__/ subdirectory adjacent to the module we want to mock.

Mock files in the __mocks__/ subdirectory are used to automock the modules they are adjacent to when the module is mocked with jest.mock(). This is useful when dealing with a lot of repetition in setting up mocks such as when mocking common dependencies or configuration objects because it makes writing a mock factory function unnecessary.

Assuming a common configuration file that is used by many different modules, mocking it would look like this:

// common/config.js
module.exports = { foo: "bar" };
Enter fullscreen mode Exit fullscreen mode
// common/__mocks__/config.js
module.exports = { foo: "mockBar" };
Enter fullscreen mode Exit fullscreen mode
// example.js
const config = require.("./common/config");

// Logs "bar"
module.exports = () => console.log(config.foo);
Enter fullscreen mode Exit fullscreen mode
// example.spec.js
const example = require("./example");

jest.mock("./common/config");

// Logs "mockBar", no need for a mock factory
example();
Enter fullscreen mode Exit fullscreen mode

💡Note
Remember: mocks and automocking are only in effect when running tests with Jest. They do not have an effect on the code in development or production.

That's it! We're now ready to mock imported functions with Jest.


Jest mock imported functions example code

The dependency in isInteger.js:

// isInteger.js
module.exports = (value) => Number.isSafeInteger(value);
Enter fullscreen mode Exit fullscreen mode

The unit to be tested in isAtLeast18.js:

// isAtLeast18.js
const isInteger = require("./isInteger");

module.exports = (value) => isInteger(value) && value >= 18;
Enter fullscreen mode Exit fullscreen mode

The unit test in isAtLeast18.spec.js:

// isAtLeast18.spec.js
const isAtLeast18 = require("./isAtLeast18");
const isInteger = require("./isInteger");

// The mock factory returns a mocked function
jest.mock("./isInteger", () => jest.fn());

beforeEach(() => {
  isInteger.mockClear();
});

describe("isAtLeast18", () => {
  it("fails if value is not recognised as integer", () => {
    isInteger.mockImplementation(() => false);

    expect(isAtLeast18(123)).toBe(false);
    expect(isInteger).toHaveBeenCalledWith(123);
    expect(isInteger).toHaveBeenCalledTimes(1);
  });

  it("passes if value is recognised as integer and is at least 18", () => {
    isInteger.mockImplementation(() => true);

    expect(isAtLeast18(123)).toBe(true);
    expect(isInteger).toHaveBeenCalledWith(123);
    expect(isInteger).toHaveBeenCalledTimes(1);
  });
});
Enter fullscreen mode Exit fullscreen mode

Homework and next steps

  • Write more comprehensive tests and use fixtures to cover any additional cases. If you've done your homework from the previous article, try continuing from where you left off.
  • Fix the code so any failed tests pass or write a newer, better implementation.
  • Achieve 100% code coverage in the coverage report.

Thank you for taking the time to read through this article!

Have you tried mocking imported functions with Jest before? What was your experience like?

Leave a comment and start a discussion!

Discussion (0)