Mock functions in individual tests using Jest
Tyler Smith
Posted on August 4, 2024
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 };
// CommonJS import
const getUsersList = require('./greet');
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!";
}
// ES module import
import { greet } from "./greet";
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 };
However, it is also possible to export a function by itself:
// CommonJS export
function greet() {
return "Hello, world!";
}
module.exports = greet;
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;
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);
});
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,
};
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);
});
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";
}
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");
});
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";
Then when we want to spy on the default export, we use "default"
:
jest
.spyOn(esmModule, "default")
.mockImplementation(() => "mock implementation default");
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>)
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();
});
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;
};
},
};
});
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.
Posted on August 4, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.