This content originally appeared on DEV Community and was authored by Tyler Smith
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";
// Whether or not you need this block will depend
// on what you're mocking.
jest.mock("./esmModule", () => {
const originalModule = jest.requireActual("./esmModule");
return {
__esModule: true,
...originalModule,
};
});
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 examples, with a few key differences.
First, we're importing our module as a namespace import.
import * as esmModule from "./esmModule";
This allows us to bundle the default function and all of the named functions into a single object, which we need in order to be able to use jest.spyOn()
.
After that, we have the following bit of code:
jest.mock("./esmModule", () => {
const originalModule = jest.requireActual("./esmModule");
return {
__esModule: true,
...originalModule,
};
});
This code replaces the module with a Jest ES Module mock that contains all of the module's original properties. Without this, jest.spyOn()
doesn't always work. This code may or may not be necessary in your project, depending on how Jest is configured. For my current project, it's necessary to use a jest.mock()
call like this for react-router-dom
, but unnecessary for my own ES Modules.
So how will you know when you need to use jest.mock()
when using ES modules? You'll get a TypeError
in your tests when you need it and don't have it. When I don't include the jest.mock()
block above on react-router-dom
and try to mock useNavigate()
via spy-on, I get the following error:
TypeError: Cannot redefine property: useNavigate
at Function.defineProperty (<anonymous>)
Finally, when we want to spy on the default export, we use "default"
in our code:
jest
.spyOn(esmModule, "default")
.mockImplementation(() => "mock implementation default");
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.
Let me know if I missed something or if there's something that I didn't include in this post that should be here.
This content originally appeared on DEV Community and was authored by Tyler Smith
Tyler Smith | Sciencx (2024-08-04T01:31:46+00:00) Override functions in individual tests using Jest. Retrieved from https://www.scien.cx/2024/08/04/override-functions-in-individual-tests-using-jest/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.