Home >Web Front-end >JS Tutorial >Override 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.
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.
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); });
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.
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); });
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.
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"); });
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");
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; }; }, }; });
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.
Let me know if I missed something or if there's something that I didn't include in this post that should be here.
The above is the detailed content of Override functions in individual tests using Jest. For more information, please follow other related articles on the PHP Chinese website!