How to mock a function in Jest for a React and Typescript app
A mock is a type of test double that replaces part of a codebase to make testing easier. An example of code that is often mocked is a web API call. There are a few reasons why mocking a web API call is useful:
- To ensure the web API call response is consistent - the data behind the real web API will probably change over time, causing the real web API response to vary.
- To speed up the test - web API requests are slow.
- If the web API is a third-party paid service, mocking it reduces costs.
This post covers how to mock a function that makes an API call in a React component test.
A component to test
The component to test is below. It renders a character name requested from the Star Wars API.
export function Hello({ id }: Props) {
const [character, setCharacter] = React.useState<undefined | string>(
undefined
);
React.useEffect(() => {
getCharacter(id).then((c) => setCharacter(c));
}, [id]);
if (character === undefined) {
return null;
}
return <p>Hello {character}</p>;
}
Here’s our test:
test("Should render character name", async () => {
render(<Hello id={1} />);
expect(await screen.findByText(/Bob/)).toBeInTheDocument();
});
The test fails though, because the component renders Luke Skywalker. 😞
We could change the expectation to find Luke Skywalker, but that couples the test to the data, making it brittle because the data could change over time.
We will mock getCharacter
and make it return "Bob"
to resolve the problem.
First attempt
Here’s a naive attempt:
import { getCharacter } from "./data";
test("Should render character name", async () => {
const safe = getCharacter;
// 💥 Cannot assign to 'getCharacter' because it is an import
getCharacter = (id) => {
return new Promise((resolve) => resolve("Bob"));
};
render(<Hello id={1} />);
expect(await screen.findByText(/Bob/)).toBeInTheDocument();
// 💥 Cannot assign to 'getCharacter' because it is an import
getCharacter = safe;
});
This is far from perfect though. The main problem is that it errors when a new value is assigned to the imported function.
Second attempt
The import * as name
syntax imports the whole module in an object structure. Maybe we’ll be able to mock a function from a module if we import it this way?
If we import the getCharacter
as follows:
import * as data from "./data";
… getCharacter
can be accessed as data.getCharacter
.
Let’s refactor the test to use this approach:
import * as data from "./data";
test("Should render character name", async () => {
const safe = data.getCharacter;
// 💥 Cannot assign to 'getCharacter' because it is a read-only property
data.getCharacter = (id) => { return new Promise((resolve) => resolve("Bob"));
};
render(<Hello id={1} />);
expect(await screen.findByText(/Bob/)).toBeInTheDocument();
// 💥 Cannot assign to 'getCharacter' because it is a read-only property
data.getCharacter = safe;});
This still doesn’t work. 😞
However, it feels like we’ve made a step forward because it isn’t saying we can’t mock the import - it’s saying we can’t mock getCharacter
because it is read-only.
Using jest.spyOn
Jest has a spyOn
function, which can resolve the problem and make the test much better.
jest.spyOn
allows a method in an object to be mocked. The syntax is:
jest.spyOn(object, methodName);
It also has a mockResolvedValue
method to provide the resolved return value.
Let’s refactor the test to use this approach:
test("Should render character name", async () => {
const mock = jest.spyOn(data, "getCharacter").mockResolvedValue("Bob");
render(<Hello id={1} />);
expect(await screen.findByText(/Bob/)).toBeInTheDocument();
mock.mockRestore();
});
Notice also that spyOn
also has a mockRestore
method to restore the original implementation at the end of the test.
The test now works!
We can do a little better though. We can use the toHaveBeenCalled*
expectations to verify the mock is called:
test("Should render character name", async () => {
const mock = jest.spyOn(data, "getCharacter").mockResolvedValue("Bob");
render(<Hello id={1} />);
expect(await screen.findByText(/Bob/)).toBeInTheDocument();
expect(mock).toHaveBeenCalledTimes(1);
expect(mock).toHaveBeenCalledWith(1);
mock.mockRestore();
});
Nice! ☺️
The code from this post is available in Codesandbox in the link below.
Did you find this post useful?
Let me know by sharing it on Twitter.If you to learn more about testing React apps, you may find my course useful: