Unit testing async code in React with Jest

HS
5 min readMar 11, 2021

Let’s get straight to the point. What we’ll be doing here is write a bit of code, asynchronous code in particular, and write a test for that piece of async code.

Disclaimer: This may be an over simplified example, but it’s in understanding the concept being presented in this post than the literal implementation of the same.

Scenario:

  1. Write some async code to fetch data from an api
  2. Write code to present that data on a webpage
  3. Write a test for ☝️.

For simplicity’s sake, we’ll just create a new Create React App project which will install Jest and also the react testing library.

Make sure you’ve got Create React App installed globally. If you don’t already have it installed, have a look at this link.

To create a new project, open a terminal of your choice and navigate to your desired location. Then, enter the following command to create a project with the name jest-test or any name of your choosing:

npx create-react-app jest-test

Open the project in your Code Editor of choice and replace the code in the src/App.js file with the following:

import { useState, useEffect } from 'react';
import axios from 'axios';
function App() {
// Store all todos
const [todos, setTodos] = useState([]);
useEffect(() => {
// Get todos from an api
axios.get('https://jsonplaceholder.typicode.com/todos')
.then((response) => {
const todos = response.data;
setTodos(todos);
})
}, [])
return (
<div className="App">
{
// Render todos, if any
todos.length && todos.map((todo) => (
<p>
{ todo.title }
</p>
))
}
</div>
);
}
export default App;

All we’ve done in the code above is told our app to fetch some todos from an api (it’s a public api so feel free to use it), and store them in the todos state variable. Then we just iterate over the todos list and render the title of each todo inside the <p> tag.

You can confirm that the todos are rendering in the browser by running:

yarn start

You should see something similar to the following in your browser:

List of todos
List of todos

Now, the fun bit. Let’s test that the code which is fetching the todos from the api is actually getting executed and is returning the todos.

Firstly, create a file called mockData.js in the src folder and paste the following in the file. These are just some mock todos that we’ll be using in our tests. We DO NOT want to make a real api call in our tests. No Sirree!

export const mockTodos = [
{
"title": "The first todo",
"completed": false
},
{
"title": "The second todo",
"completed": false
}
]

Open the App.test.js, and replace the code with the following. We’ll go over it in bits.

import { act, render, waitFor } from '@testing-library/react';
import App from './App';
import { mockTodos } from './mock';
import axios from 'axios';
// Mock the axios module
jest
.mock('axios');
test('renders todos', async () => {
// Set up a mock response for when axios.get is called in the component
// Should return the 2 mock todos from our mock data
await act(() => Promise.resolve(
axios.get
.mockResolvedValueOnce(
{data: mockTodos}
)));
// We don't need to do much other than render the component
// The api call is executed in the useEffect hook on initial render
// Which will return the mock response from the above line of code
const
{ getByText } = render(<App />);
// WaitFor ensures the content is rendered before running assertions
// or times out
await waitFor(() => {
expect(axios.get).toBeCalledWith('https://jsonplaceholder.typicode.com/todos');
expect(getByText('The first todo')).toBeVisible();
expect(getByText('The second todo')).toBeVisible();
})
});

Let’s look at what’s going on in the code above.

import axios from 'axios';// Mock the axios module
jest
.mock('axios');

We first import the axios module, and then we make sure to mock it. Following is a snippet straight from the Jest documentation for why we need to mock the module:

Once we mock the module we can provide a mockResolvedValue for .get that returns the data we want our test to assert against. In effect, we are saying that we want axios.get to return a fake response.

Inside the async test() block, the first thing we do is set up the mock response value. As stated in the snippet above, we’re making sure that when axios.get is called in the component, we return a mock response, which in our case are the mockTodos.

await act(() => Promise.resolve(
axios.get
.mockResolvedValueOnce({data: mockTodos})
));

Because the code above performs a state update in the component which in turn causes the component to re-render, we need to wrap it inside act(…). It ensures that the Promise is resolved, and the component is ready before any assertions are made.

The callback passed to act(…) must return undefined, or a Promise, and it’s generally a good idea to await an act(…), especially if you have multiple act(…) calls in the same test. Our test should still pass even if you don’t use await, though you’ll see a warning in the console on running the test.

We then render the <App /> component using the render function from the @testing-library/react package, from which we get the getByText function using object destucturing.

const { getByText } = render(<App />);

getByText allows us to get an element based on the text value of the element.

In the following snippet, we assert that the api was called with the correct url and the content we expect is actually visible. Content, in this case, being the Todo titles from the mockTodos.

await waitFor(() => {
expect(axios.get).toBeCalledWith('https://jsonplaceholder.typicode.com/todos');
expect(getByText('The first todo')).toBeVisible();
expect(getByText('The second todo')).toBeVisible();
})

waitFor, waits for the UI to be ready and keeps retrying the callback passed to it until the callback stops throwing or times out. This is an important detail which, if missed, will cause your tests to fail as the assertions will be run before the act(…) call is fully resolved and the UI actually renders the data.

This was a simple example of how we might test some async code in our app by mocking a third party module. You could, if you wanted to, have your own wrapper that returns an instance of axios and you could then mock that in your test, but the fundamental concept of mocking and testing the code remains the same.

--

--

HS

Full Stack Developer based in Melbourne, Australia