Manual Mocks in Jest
Level up your unit tests
Table of Contents
– Intro
– Jest Mock Method
– The Mocked Function Object
– Mock Factories
– Composition
Intro
Mocking modules in itself is not hard. However, in my experience, mocking code can become overly complicated and be a drain on engineering time. Although it is not the most exciting topic, it is worth spending time understanding the different ways to mock a module. If a developer has a solid understanding of the mock function, it can serve as a cornerstone of their test strategy and speed up time spent writing tests.
Part of my reason for writing this article is selfishness. I often get tripped up by nuances involved in mocking a module and end up on stack overflow or combing through the Jest documentation, so I wanted to create a reference to use in the future. I hope you also find it helpful.
The methods outlined here are not front or backend specific, but my examples assume you use ES modules alongside typescript. They will still work if you have a different setup, but you may need to modify them appropriately.
Jest Mock Method
In this article, I will look at two techniques for mocking. The first technique will call the mock function with one argument, while the second involves calling the mock function with two arguments. Both methods do similar things. The second parameter (a factory function) allows for more customisations and control.
Let’s look at technique one, calling the mock function with one argument.
jest.mock("../lib/track")
Here I am mocking a module which contains one default export. The default behaviour is to set a module’s default and non-default exports to a Jest function object. Later the article will cover the factory method in more depth, but let’s briefly look at it to help show how the object will look.
jest.mock("../lib/track", () => {
return {
__esModule: true,
default: jest.fn(), // default is set to a mock function object
};
});
Both methods do the same thing. But in the second, we explicitly set the mock implementation via a factory function.
Now that we have scratched the surface of the mock method, it would be an excellent time to dive into more detail about the mocked function object.
The Mocked Function Object
The mock function object contains methods for mocking return values and spying on function calls. We will look at 3 key features of the mocked function object: accessing it, spying on it and setting mock implementations.
- Accessing the mocked function
Mocking a module sets the module’s properties to a mock function object. However, we need a way to access this object. To access the mock function object, import the mocked module at the top of the file.
import track from "../lib/track"jest.mock("../lib/track")
In the example above, track
is a mocked function. However, before we use the method to make assertions in our tests, we need to do one last bit of housekeeping. Typescript will not be able to pick up that this is a mocked function; to take full advantage of the type system, we need to use the mocked function from Jest.
const mockTrack = jest.mocked(track)
The mocked method tells the editor that this is now a mocked function.
2. Spying on arguments
The code is now ready to make assertions on:
expect(track).toBeCalled()
Because track
is a void function, there is no need to set a return type. In the above example, Jest spies on the mocked function.
3. Mock implementation and return types
When a module needs to return a data type, the mock function methods can be used to mock the return value or the implementation. Refer to the Jest documentation for a complete list; I frequently use mockReturnValue
, mockResolvedValue
, mockRejectedValue
and mockImplementation
.
Let’s take a look at how this works in practice.
mockFetchData.mockResolvedValue(['hey'])
In the test itself, mockResolvedValue
is used to set a predictable return type which can then be tested in the “expect” clause.
The same technique works for non-default exports. First, import the non-default export at the top of the test:
import { refetchData } from "../lib/fetchData"
Then it can be mocked using precisely the same method as the default. Start using the jest.mocked
approach to ensure the correct typings.
const mockRefetchData = jest.mocked(refetchData)
And then, the return value can be mocked using the mockResolvedValue method as this is an async function.
mockRefetchData.mockResolvedValue(["refetch"])
Here is the complete code sample for both examples:
Mock Factories
As well as setting return types using inline functions, it is possible to use a factory function in the mock constructor. A factory could be a good choice if the return type does not need to change throughout the test.
Using factories, there are a couple of syntactical quirks to be aware of.
Start by importing the module at the top of the file:
import * as fetchData from "../lib/fetchData"
Notice the import using the * as
syntax. I think this is because jest.mock
is designed to be used with commonJS, and using the * as
creates a type of object more in line with that standard.
Now let’s look at the arguments for the jest.mock
function.
jest.mock<typeof fetchData>("../lib/fetchData", () => { ...
The path in the first argument is the same as the auto-mocking methods in the last section. The second argument is where all the action is; it’s a factory function which will return an implementation of the module. The module implementation has a lot of flexibility. You can set all the exports to bear a jest.fn
or just some of them, set specific methods to simplified or noop procedures and require the original method implementation. You can also mix all of these to create different combinations. Now let’s look at the types of properties a mocked modules method can be set to:
- Mock functions
Here’s the process with the default mocked implementation members set to jest.fn:
jest.mock<typeof fetchData>("../lib/fetchData", () => {
return {
__esModule: true,
default: jest.fn(() => Promise.resolve(["hey])),
};
});
Note the funky __esModule
syntax and assignment to default as per the Jest docs:
“ This property is normally generated by Babel / TypeScript, but here it needs to be set manually.”
The default property is added to represent the default function. To add a non-default method, just add the name of that method. In the example below, the refetchData
property shows how to do this:
jest.mock<typeof fetchData>("../lib/fetchData", () => {
return {
__esModule: true,
default: jest.fn(() => Promise.resolve(["hey])),
refetchData: jest.fn(() => Promise.resolve(["refetch"])),
};
});
2. Normal functions
For both the default and non-default, we are setting it to the jest.fn object. However, it is not essential to set this property to a jest.fn
; depending on the use case, a more straightforward method could be used. If you don’t intend to spy on the function, try using a simpler one.
jest.mock<typeof fetchData>("../lib/fetchData", () => {
return {
__esModule: true,
default: async () => "simples",
refetchData: jest.fn(() => Promise.resolve(["refetch"])),
};
});
In the code above, the default is set to a simple implementation. We would test this implicitly by checking the return result of getData,
const data = await getData(false)expect(data).toEqual("simples")
unlike the refetchData
property, which is set to the jest.fn
object and which would be tested explicitly via checking what arguments it was called with:
expect(mockRefetchData).toBeCalledWith(true, {max: 100, min: 5})
3. Original module
In rare scenarios, you may want to only mock part of a module. To do this, Jest has a function called requireActual
, which needs to be used inside the factory function:
jest.mock<typeof fetchData>("../lib/fetchData", () => {
return {
__esModule: true,
default: jest.requireActual("../lib/fetchData").default,
refetchData: jest.fn(() => Promise.resolve(["refetch"])),
};
});
The default is set to the default export of fetch data, and “refetchData” is set to a jest.fn
.
Here are the code samples from this section (with the first default mock and the simple mock commented out) so far:
4. Pointers
The final factory property is when we want to set a mocked module property to a named variable:
const mockMethod = jest.fn()jest.mock('path', () => {
default: mockMethod // This will not always work!
...
Have a look at the code above; looks legit right? It’s conceptually the same as using an anonymous mocked function. However, the Jest mock method gets hoisted to the top of the file by most compilers. In some scenarios trying to run this code will result in an error that says, “Cannot access before initialisation”.
I have found no good way around this. The first option is to define the const at the top of the file before the imports.
const mockRefetchData = jest.fn()import * as fetchData from "../lib/fetchData";jest.mock<typeof fetchData>("../lib/fetchData", () => {
return {
refetchData: mockRefetchData,
...
The other option is to use the doMock
function, which explicitly avoids the hoisting behaviour. However, note that you will need to define the mock before the imports, which looks a bit weird:
const mockRefetchData = jest.fn()/**
* * manual mock with factory
*/
jest.doMock<typeof fetchData>("../lib/fetchData", () => {
return {
refetchData: mockRefetchData,
};
});import * as fetchData from "../lib/fetchData";
...
In this case, combining techniques from the default mocking mentioned earlier in the article with the factory implementation would be better. In the final section, this is what I will now look at.
Composition
We have seen how using factories can be a good way of grouping mocked methods but can be challenging if you require a pointer to spy on a process. At the start of the article, we used imports along with the Jest mocked and mock return type functions. Let’s combine these approaches to achieve a better way of spying on factory methods.
Start by importing the module:
import * as fetchData from "../lib/fetchData";
Create a factory and set a default implementation.
jest.mock<typeof fetchData>("../lib/fetchData", () => {
return {
__esModule: true,
refetchData: jest.fn(() => Promise.resolve("default"))
...
Let’s use the jest.mocked
method to create a pointer (like we did earlier in the article).
const mockRefetchData = jest.mocked(fetchData.refetchData);
In our tests, refetchData
will return “default” by default, but we can use the mockRefetchData const to access the jest.fn
and spy on and modify its return value.
Here is the complete example