You’ve probably found your way to this post because you're trying to create mock code for tests that use the Jest framework and are having difficulty getting your mock to work the way you want. This post focuses on why you might choose one Jest method over another to create a mock—and how to use it—so you can work and write tests more quickly.
What is a mock?
A "mock" is code that replaces actual functionality in a test environment. Mock code is inserted at an "interface" between the "code unit" being tested and the code external to the current code unit.
A "code unit" is code that encapsulates a set of functionality; for example, a function or class. This is an elastic term that is based on context. A React component that fetches data for display might be considered a code unit.
Why use a mock?
Usually, it's better not to create mock code. However, there are cases where it's particularly helpful:
-
Limit testing to a specific code unit
-
Provide specific inputs or outputs during a test
-
Simplify the amount of setup required to run a test
-
Verify the inputs and outputs at interfaces between code units
When you write mock code, here are a few guidelines:
-
Keep the mock functionality to the bare minimum required to accomplish testing.
-
Create well-defined interfaces; this is a good coding practice at all times.
-
Insert Mocks at those well-defined interfaces.
Jest Mock methods
This table summarizes the capabilities of the methods Jest provides to create a Mock.
Creator Function |
Mock Specific Module Function |
Must Import Actual Module |
May Include Actual Functionality |
Restore Actual Functionality |
Complexity |
---|---|---|---|---|---|
|
no |
N/A |
no |
yes |
medium |
|
yes |
yes |
yes |
yes |
low |
|
no |
no |
no |
no |
low |
|
yes |
no |
yes |
no |
high |
Example code
Let's look at the example code that will be used throughout this post to illustrate mocks and testing.
We have a file named spaceship.ts with three exported functions: plotCourse
, startLightSpeedJump
, and driveErrors
.
The plotCourse
and startLightSpeedJump
functions make calls to exported functions in a file named navigation.ts. The third function, driveErrors
, invokes exported functions in hyperdrive.ts.
You need to write tests for the code in spaceship.ts, and you want to mock code in the other files. Navigation code is complex and requires extensive configuration and setup, as you might expect when plotting an interstellar course for a spaceship. You also need to verify that the calls to the functions in navigation.ts and hyperdrive.ts are done correctly by code in spaceship.ts.
All set? Great! Now we’ll take a look at each of the Jest methods from the table above and see how each can be used in testing.
Note that the code examples that follow do not include all the contents of a syntactically correct Jest test file. The examples only include lines of code that are relevant to what is being described.
jest.fn
When you need to mock a single function that is the value of a variable—e.g., a const or an argument to another function —use jest.fn
. In its simplest form, jest.fn
creates a default Mock function that will accept any parameters and returns undefined
.
Once you have a Mock instance, there are two primary ways to customize it: provide a result, or provide an implementation that returns a result.
Return a specific result using mockReturnValue
, regardless of the arguments provided. Mock also supports async
functions via mockResolvedValue
for success or mockRejectedValue
to return an error.
It's also possible for you to mock the entire function implementation. The mock implementation is a callback function that can be provided as the argument to jest.fn
or explicitly using mockImplementation
. The callback will be invoked by the Mock and will receive all the actual arguments, and may return a value.
In the following test code, the function startLightSpeedJump
accepts a callback function as its only parameter. In this example, a Mock is provided as the argument, and the Mock is tested to see if it was called as required: it should have been invoked one time with a single argument with the value "Abort?"
// spaceship.spec.ts
test("onWarning is invoked with abort query", async () => {
// This Mock will always return false when it is invoked.
const mockOnWarning = jest.fn().mockReturnValue(false);
// The Mock function is passed as the callback for the parameter
// `onWarning`. Later we can test to see if it was invoked properly.
await startLightSpeedJump(mockOnWarning);
expect(mockOnWarning).toHaveBeenCalledTimes(1);
expect(mockOnWarning).toHaveBeenLastCalledWith("Abort?");
});
jest.spyOn
If you need to verify the invocation of one specific function exported from a module while maintaining all of the module's actual functionality, then choose jest.spyOn
(you can use spyOn
with any module export, such as a Class).
The entire module must be imported into the test file—typically using the import * as module
syntax. Note that by default, a Mock created by spyOn
will invoke the module's actual implementation. However, like any Mock, its return value or implementation can be altered using mockReturnValue
or mockImplementation
.
In the following example, we need to verify that plotNavigation
in spaceship.ts properly calls the calculateRoute
function in navigation.ts. The entire navigation module is imported and spyOn
is used to insert a Mock in the navigation module for just the calculateRoute
function. Later in a test, we verify that mockCalculateRoute
was invoked one time with the correct arguments.
// spaceship.spec.ts
import * as nav from "./navigation";
// Every property in the "navigation" module will have its actual
// implementation except `calculateRoute` which will be a Mock that
// invokes the actual implementation. `spyOn` returns a Mock for the
// targeted function.
const mockCalculateRoute = jest.spyOn(nav, "calculateRoute");
// spaceship.spec.ts
const result = plotNavigation({ uid: 1 }, { uid: 2 }, 60);
// Verify that `plotCourse` invoked the mocked `calculateRoute`
// function with the correct arguments.
expect(mockCalculateRoute).toHaveBeenCalledTimes(1);
expect(mockCalculateRoute).toHaveBeenLastCalledWith(
{ uid: 1 },
{ uid: 2 },
3600,
expect.any(Function)
);
jest.mock
To replace all of the exports in a module with Mocks, you should start with jest.mock
’s single argument signature. The single argument signature is the simplest way to eliminate all the code not being tested contained in a module. The other scenarios use jest.mock
's two-argument signature and encompass a variety of complex situations where mock
is helpful:
-
Reduce or eliminate setup for complex functionality
-
Invocations of callbacks in mocked code
-
Use actual code
Let's take a look at both of these scenarios in turn.
Simple module mocking
Simple mocking is quick, and ideally, you never need to do anything beyond mocking the module! However, since each export in the module has been replaced with a Mock, the behavior of each export can be changed if needed. Remember that by default, each Mock generated by mock
will return undefined
.
In the example below, we need to make sure that plotCourse
in spaceship.ts properly invokes the calculateRoute
function in navigation.ts, but we do not care about the result.
In tests prefer to verify the result returned from an interface invocation, but verifying the parameters is also useful to prevent an interface from being inadvertently changed.
First, you use mock
so that every export in the navigation module is mocked. Because each export is a Mock, it's possible to use them in an expect
statement with Mock-related matchers like toHaveBeenCalledTimes
. In the test, we examine the Mock to verify that calculateRoute
is invoked correctly.
Note that the module-name must be identical in both the import
statement and the argument to jest.mock
.
// spaceship.spec.ts
import { calculateRoute } from "./navigation";
// Every function provided by the "navigation" module will have a
// default Mock.
jest.mock("./navigation");
test("plotCourse converts departure time to seconds", () => {
const result = plotCourse({ uid: 9 }, { uid: 1 }, 23);
// Since the entire "navigation" module has been mocked the imported
// `calculateRoute` is a Mock.
expect(calculateRoute).toHaveBeenCalledTimes(1);
expect(calculateRoute).toHaveBeenLastCalledWith(
expect.any(Object),
expect.any(Object),
1380,
expect.any(Function)
);
// All the exported functions in the "navigation" module have been
// mocked and they all return `undefined`. Because of that `jumps`
// which is calculated by those exported (and now mocked) functions
// will be `undefined`.
expect(result).toEqual({
arrivalDateTimeMinutes: -1,
jumps: undefined,
});
});
If necessary, we could alter the calculateRoute
Mock, like any other Mock, to provide an implementation or return a specific value.
If your code is written in TypeScript—and depending on your tsconfig settings—you may need to cast your imported function to the jest.Mock
interface for type checking to complete successfully when you try to use functionality that is part of the Mock interface.
Complex module mocking
Complex module mocking is useful when you need to combine mocked data with functionality that is part of the actual module, or it's easier to simulate functionality using a mocked function rather than configuring the actual code to produce a desired result.
A common “gotcha” when mocking code is attempting to use variables or functions inside mock
’s callback function body that are defined outside mock
. This causes an error because of the code execution order. When a test run starts, Jest "hoists" functions that create module Mocks to the top of the code execution order and then resolves all the modules, both from import
and module mocks.
Part of Jest's resolution is executing a mock
’s callback function. When that happens, variables outside the body of the callback have not yet been initialized—the runtime knows they exist, but they do not yet have a value. You may have encountered this problem when trying to use a Mock defined outside mock
, inside the callback.
// spaceship.spec.ts
const mockCalculateRoute = jest.fn();
jest.mock("./navigation", () => {
// Error! `mockCalculateRoute` doesn't have a value yet.
const calculateRoute = mockCalculateRoute.mockImplementation(() => {
/* Mock functionality... */
});
return {
calculateRoute,
};
});
Running this code results in an error:
ReferenceError: Cannot access 'mockCalculateRoute' before initialization
There are many ways you can implement this functionality if it’s needed. One solution is to set up the Mock in the body of the exported mock function:
// spaceship.spec.ts
const mockCalculateRoute = jest.fn();
jest.mock("./navigation", () => {
return {
// Return a function for the module export. The function will not
// be constructed until a test runs, at that point
// mockCalculateRoute has a value and it is a Mock.
calculateRoute: (...args) => {
// Set the implementation for the mock and then invoke the
// Mock's implementation with `args`.
mockCalculateRoute.mockImplementation(() => {
/* Mock functionality... */
})(...args),
}
};
});
Using a module’s actual functionality
There are situations when you are creating a module mock where it is helpful to use the functionality of the “actual” (not mocked) module. Fortunately, Jest provides a function named requireActual
that returns the actual module.
There are two primary uses of the actual module. One use is to include the actual module exports in the mocked module using the spread operator, which allows you to mock only selected exports while maintaining the actual code for the remaining exports. Another use is a situation where reusing actual functionality in your mock code makes the mock code simpler or eliminates duplicate code.
Now that you have a better understanding of how Jest loads modules and also how to access the actual functionality of a module, let’s look at constructing a module mock.
Your first module mock
In the following scenario, we need to test the invocation of a callback function that is passed from the module being tested to another module. You can mock the module that receives the callback and invoke the callback as needed.
In the code sample below, the spaceship.ts function plotCourse
invokes a function in navigation.ts named calculateRoute
. One of the parameters to calculateRoute
is a callback function named setLastError
that allows calculateRoute
to provide error information.
We want to test the code in spaceship.ts that handles errors from setLastError
. Normally it would take a lot of effort to properly setup and configure context to cause an error to be generated to cause setLastError
to be invoked, but since our test is concerned with how spaceship.ts handles errors and not about how navigation.ts generates them we can use a module Mock to simplify our testing.
You start by using the signature of jest.mock
that accepts two parameters. The first parameter is the module-name to mock, the second parameter is a function that returns a module object. In this case we've decided to spread the actual module's exported functionality into the mocked module with one exception - we're providing our own implementation for calculateRoute
. We use the actual errorIdToErrorData
function to exchange an error ID for error data. Now we can provide error data back to spaceship.ts code by invoking setLastError
with the error data.
// spaceship.spec.ts
jest.mock("./navigation", () => {
// actualNavigationModule is the actual un-mocked module.
const actualNavigationModule = jest.requireActual("./navigation");
// The object returned is our mocked module.
return {
// Mostly the actual module's properties will be returned...
...actualNavigationModule,
// ...except `calculateRoute` will be overridden with a new mock
// implementation that alters the internal functionality.
calculateRoute: (
from: Planet,
to: Planet,
starDateTimeSeconds: number,
setLastError: (error: ErrorData) => void
) => {
// Get an error using the actual errorIdToErrorData function.
const err = actualNavigationModule.errorIdToErrorData(
"ERROR_MISSING_COORDINATE"
);
// Set the lastError data to an error state for test purposes.
setLastError(err);
},
};
});
Now our test code expects that a lastError
property will exist on the result of plotCourse
and lastError
will have the expected error data.
// spaceship.spec.ts
test("plotCourse handles 'ERROR_MISSING_COORDINATE' error", () => {
// `plotCourse` will invoke the mocked `calculateRoute` which does
// not return a result, however mocked `calculateRoute` does call
// `setLastError`.
const result = plotCourse({ uid: 45 }, { uid: 789 }, 678);
// We test to verify that `result` now includes error data in the
// `lastError` property.
expect(result).toEqual({
arrivalDateTimeMinutes: -1,
jumps: undefined,
lastError: {
helpMessage: "Have you tired rebooting?",
id: "ERROR_MISSING_COORDINATE",
title: "An error occurred",
},
});
});
Using variables in mocked modules
You may need to have the mocked module provide different results without changing the internal mock functionality —e.g., avoid duplicating code that recreates the same mock implementation in multiple tests or testing combinations of one or more variables in a single test.
To achieve this, create a variable outside the mocked implementation and then change its value before executing the test code. Building off the previous example, we need to verify the handling of every possible error that can happen. One way to verify error handling is to create a variable that gets set at test time. You might be tempted to create a variable and then reference that variable in the mock code like this:
// spaceship.spec.ts
let errorId: string | undefined;
// spaceship.spec.ts
// The value to test is provided by `errorId` which can be changed for
// each test.
const err = actualNavigationModule.errorIdToErrorData(errorId);
In your test (or a beforeEach
or beforeAll
function) you set the value of errorId
:
// spaceship.spec.ts
errorId = "ERROR_GRAVITY_WELL";
But there's a problem - when you run this test, it fails with an error:
The module factory of
jest.mock()
is not allowed to reference any out-of-scope variables.
Fortunately, there is a solution noted in the error message:
If it is ensured that the mock is required lazily, variable names prefixed with
mock
(case insensitive) are permitted.
This means that if you intend to set the value of a variable used in mock code—outside the mock code via beforeEach
, beforeAll
, or the test itself—that's okay. You just have to prefix the name of the variable with "mock."
So rename errorId
to mockErrorId
.
// spaceship.spec.ts
// Prefix the varible name with "mock".
let mockErrorId: string | undefined;
jest.mock("./navigation", () => {
const actualNavigationModule = jest.requireActual("./navigation");
return {
...actualNavigationModule,
calculateRoute: (
from: Planet,
to: Planet,
starDateTimeSeconds: number,
setLastError: (error: ErrorData) => void
) => {
// The value to test is provided by `mockErrorId` which can be
// changed for each test.
const err =
actualNavigationModule.errorIdToErrorData(mockErrorId);
setLastError(err);
},
};
});
Set the value of
mockErrorId
at the beginning of the test, and now the test runs successfully.
// spaceship.spec.ts
test("plotCourse handles 'ERROR_GRAVITY_WELL' error", () => {
// Set the value that we want the test code to verify.
mockErrorId = "ERROR_GRAVITY_WELL";
const result = plotCourse({ uid: 567 }, { uid: 4902 }, 10000);
// We test to verify that `result` now includes error data based on
// the value of `mockErrorId`.
expect(result).toEqual({
arrivalDateTimeMinutes: -1,
jumps: undefined,
lastError: {
helpMessage: "Choose a different planet for best results.",
id: "ERROR_GRAVITY_WELL",
title: "An error occurred",
},
});
});
Verify mocked module calls
It's not unusual to write tests that have multiple needs: simplify code that is behind an interface, use actual code in that simplified code, and verify that the code under test uses the interface properly. You can do all those things by updating the previous example to use a Mock.
// spaceship.spec.ts
// Create a Mock to use in the body of the mocked module.
const mockCalculateRoute = jest.fn();
jest.mock("./navigation", () => {
const actualNavigationModule = jest.requireActual("./navigation");
return {
...actualNavigationModule,
// Capture all of the arguments that are passed to `calculateRoute`.
// They will be used later when the Mock is invoked.
calculateRoute: (...args) =>
// Every time `calculateRoute` is invoked create a new mock
// implementation; the value to test is provided by `mockErrorId`
// which can be changed for each test.
//
// Immediately invoke the Mock implementation using the arguments
// passed to `calculateRoute`.
mockCalculateRoute.mockImplementation(
(
from: Planet,
to: Planet,
starDateTimeSeconds: number,
setLastError: (error: ErrorData) => void
) => {
const err =
actualNavigationModule.errorIdToErrorData(mockErrorId);
setLastError(err);
}
)(...args),
};
});
Now in the code of a test, we can examine the mock to see how it was invoked. We may also want to test the result to make sure that our mock code actually returns what's expected.
// spaceship.spec.ts
// We expect that our Mock was invoked with the correct number and
// types of arguments.
expect(mockCalculateRoute).toHaveBeenCalledTimes(1);
expect(mockCalculateRoute).toHaveBeenLastCalledWith(
{ uid: 567 },
{ uid: 4902 },
600000,
expect.any(Function)
);
// And that the Mock returned the result that was expected.
expect(mockCalculateRoute).toHaveReturnedWith(undefined);
Mocking a default export
So far, we’ve mocked modules that only have named exports. However, you’ll also encounter modules that have a default export, and mocking default exports is a little different.
In this example, we want to test driveErrors
in spaceship.ts which uses two exports from the hyperdrive.ts file: the default export and diagnostics
. We want to verify that the spaceship produces the correct collection of warnings for the hyperdrive. The default export of hyperdrive.ts returns an object with information about the hyperdrive. We want to mock the default export so it simply returns a hyperdrive object in a state that should generate warnings.
jest.mock("./hyperdrive", () => {
const actualModule = jest.requireActual("./hyperdrive");
return {
// The `__esModule` property is required when:
// - the actual module has a default export
// - the actual module is not spread in the return value of the
// Mock
// - the default export is being mocked
__esModule: true,
// The default export returns a `temperature` value that should
// generate a warning.
default: () => ({
active: true,
temperature: 200,
}),
// Return the actual `diagnostics` function.
diagnostics: actualModule.diagnostics,
};
});
A notable detail is the inclusion of the __esModule
property, which signals to Jest that the mocked module can have a default export and that the value of the property named "default" should be returned as the default export. Without the __esModule
property, the test will fail to run and will display an error:
<module name>.default
is not a function.
Bonus material! Mocking React custom hooks and functional components
The Jest framework is the preferred way to test React code, and you may wonder if it’s possible to mock React components and custom hooks. The answer is emphatically “Yes!” When custom hooks and React functional components are exported from a module, they can be replaced by a Mock.
Often React custom hooks encapsulate complex behavior that relies on state and Context; creating and managing those objects simply to get a desired result from a hook can be difficult and frustrating. In the following example, a component with the text "Nav" is included in the UI if the user is authorized to access it; the authorization status is provided by the custom hook named useAuthorization
. For test purposes, we import the useAuthorization.ts module and replace the exported hook with a Mock using spyOn
, then set the Mock's return value as needed to run the test.
// Helm.spec.tsx
import * as useAuthorization from "./useAuthorization";
const mockUseAuthorization = jest.spyOn(
useAuthorization,
"useAuthorization"
);
// Helm.spec.tsx
test("NavigationPanel renders when authorized", () => {
// Verify that Nav does not exist if the user is not authorized.
mockUseAuthorization.mockReturnValue({ authorized: false });
const { rerender } = render(<Helm />);
expect(screen.queryByText("Nav")).not.toBeInTheDocument();
// Change the user to authorized and verify that Nav is displayed.
mockUseAuthorization.mockReturnValue({ authorized: true });
rerender(<Helm />);
expect(screen.getByText("Nav")).toBeInTheDocument();
});
When you test a React functional component that is comprised of multiple child components, it can be useful to replace one or more of those child components with a Mock. This is common when child components —whose functionality is outside the scope of the current test—require a complex state or context to function properly. Below, the Helm
component contains a child component NavigationPanel
that meets those criteria. Since Helm
is the component being tested, we can replace NavigationPanel
with a Mock that just returns the string "MOCK_NAV." Later we examine the Mock to verify that the NavigationPanel
was provided the correct props.
// Helm.spec.ts
// NavigationPanel will be an instance of a Mock.
import { NavigationPanel } from "./NavigationPanel";
jest.mock("./NavigationPanel", () => ({
// Return a mocked module where the React component NavigationPanel
// is replaced with a Mock instance that returns the value "Nav".
NavigationPanel: jest.fn().mockReturnValue("MOCK_NAV"),
}));
// Helm.spec.ts
test("NavigationPanel renders when authorized", () => {
mockUseAuthorization.mockReturnValue({ authorized: true });
render(<Helm />);
expect(screen.getByText("MOCK_NAV")).toBeInTheDocument();
// Verify that Helm invoked the NavigationPanel with the correct
// props including a callback function for `onWarning`.
expect(NavigationPanel).toHaveBeenCalledTimes(1);
expect(NavigationPanel).toHaveBeenLastCalledWith(
{
onWarning: expect.any(Function),
},
{}
);
});
Conclusion
Jest mocks can be complex, but they're an incredibly useful way to test your code. Choosing the right mock might require a bit of extra time and effort, but the payoff is a mock that functions as expected and allows you to test more swiftly.
Have more questions?
We'd love to hear them! Head over to our Community Discord and post them in our #ask-help channel. Our quality engineering consulting experts would be happy to assist you in any way possible. Happy coding 😄
Previous Post
Next Post