Carl Rippon

Building SPAs

Carl Rippon
BlogBooks / CoursesAbout
This site uses cookies. Click here to find out more

How to mock a function in Jest for a React and Typescript app

January 14, 2022
reacttypescriptjest

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.

Mock a function in Jest for a React and Typescript app

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.

🏃 Play with the code

Did you find this post useful?

Let me know by sharing it on Twitter.
Click here to share this post on Twitter

If you to learn more about using TypeScript with React, you may find my course useful:

Using TypeScript with React

Using TypeScript with React
Find out more