![]()
Testing asynchronous code is essential in modern React applications, especially when dealing with API calls, state updates, or other side effects. React components often need to handle asynchronous operations, such as fetching data from an API or waiting for user input. In these cases, you must ensure that your tests wait for the asynchronous operations to complete before asserting results.
Here’s a guide on how to test asynchronous code in React applications using Jest and React Testing Library.
1. Understanding Asynchronous Testing in React
In React, asynchronous operations often involve:
- API requests (e.g., fetching data).
- Event handlers (e.g., submitting a form or button click).
- Timers (e.g.,
setTimeoutorsetInterval). - State updates that happen asynchronously (e.g., through
useEffect).
When writing tests for asynchronous behavior, you need to make sure your tests wait for these operations to finish before making assertions.
2. Testing Asynchronous Code with React Testing Library
React Testing Library works seamlessly with Jest’s async utilities to test asynchronous code. The primary methods used are:
findBy*queries (e.g.,findByText,findByRole) that wait for elements to appear asynchronously.waitForto wait for conditions to be met in your component.
3. Test Example: Fetching Data from an API
Consider a React component that fetches user data from an API and displays it. This component makes an asynchronous API call when it mounts.
Example Component
// UserProfile.js
import React, { useState, useEffect } from 'react';
const UserProfile = () => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchUser = async () => {
const response = await fetch('https://api.example.com/user');
const data = await response.json();
setUser(data);
setLoading(false);
};
fetchUser();
}, []);
if (loading) return <h1>Loading...</h1>;
return <h1>{user?.name}</h1>;
};
export default UserProfile;
This component will display “Loading…” while the data is being fetched and show the user’s name once the data is loaded.
4. Testing the Asynchronous Behavior
To test this component, we need to:
- Mock the API call to control the data returned.
- Render the component.
- Assert that the loading state is displayed first.
- Wait for the component to update with the fetched data.
Example Test for Asynchronous Data Fetching
import { render, screen, waitFor } from '@testing-library/react';
import UserProfile from './UserProfile';
// Mock the global fetch function
global.fetch = jest.fn();
describe('UserProfile', () => {
it('fetches and displays user data', async () => {
// Mock the API response
const mockUserData = { name: 'John Doe' };
fetch.mockResolvedValueOnce({
json: jest.fn().mockResolvedValueOnce(mockUserData),
});
// Render the component
render(<UserProfile />);
// Assert that "Loading..." is initially displayed
expect(screen.getByText('Loading...')).toBeInTheDocument();
// Wait for the user data to be displayed
await waitFor(() => screen.getByText('John Doe'));
// Assert that the user's name is displayed
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
});
5. Key Concepts in Testing Asynchronous Code
a) Mocking Fetch Requests
In this example, we mock the fetch API using jest.fn(). This ensures that we don’t make real API calls during tests, which is important for fast and reliable testing.
global.fetch = jest.fn();
We use mockResolvedValueOnce to simulate a successful API response with the desired data.
b) Waiting for Changes with waitFor
waitFor is a utility that allows you to wait for a specific condition to be true. It waits until the DOM is updated with the correct state, such as when data has been fetched and displayed.
await waitFor(() => screen.getByText('John Doe'));
In this case, waitFor ensures that the test doesn’t fail before the asynchronous operation completes.
c) Assertions Before and After the Async Operation
You can assert that certain elements are displayed while the component is still in its loading state ("Loading...") and once the data has been fetched and the component is updated with the new state (John Doe).
expect(screen.getByText('Loading...')).toBeInTheDocument();
expect(screen.getByText('John Doe')).toBeInTheDocument();
6. Testing Asynchronous Code with findBy* Queries
You can also use findBy* queries, which are asynchronous by default. These queries will automatically wait for the element to appear before continuing the test.
Example Using findByText
it('fetches and displays user data with findByText', async () => {
const mockUserData = { name: 'John Doe' };
fetch.mockResolvedValueOnce({
json: jest.fn().mockResolvedValueOnce(mockUserData),
});
render(<UserProfile />);
// Assert that "Loading..." is displayed initially
expect(screen.getByText('Loading...')).toBeInTheDocument();
// Wait for the user data to appear
const userName = await screen.findByText('John Doe');
// Assert that the user's name is displayed
expect(userName).toBeInTheDocument();
});
Here, findByText waits for the element to appear, removing the need for waitFor.
7. Testing Asynchronous Code with Timers
Sometimes your components depend on timers (e.g., setTimeout or setInterval). Jest provides timer mocks to control the timing in your tests.
Example with setTimeout
// DelayedMessage.js
import React, { useState, useEffect } from 'react';
const DelayedMessage = () => {
const [message, setMessage] = useState('');
useEffect(() => {
const timer = setTimeout(() => {
setMessage('Hello, World!');
}, 2000);
return () => clearTimeout(timer); // Cleanup the timer
}, []);
return <div>{message}</div>;
};
export default DelayedMessage;
Test for Delayed Message with Timer
To test this, we need to mock the timers using jest.useFakeTimers and simulate the passage of time.
import { render, screen, act } from '@testing-library/react';
import DelayedMessage from './DelayedMessage';
jest.useFakeTimers();
describe('DelayedMessage', () => {
it('displays message after 2 seconds', () => {
render(<DelayedMessage />);
// Assert that no message is initially shown
expect(screen.queryByText('Hello, World!')).toBeNull();
// Simulate the passage of 2 seconds
act(() => {
jest.advanceTimersByTime(2000); // Advance the timers by 2 seconds
});
// Assert that the message appears after the delay
expect(screen.getByText('Hello, World!')).toBeInTheDocument();
});
});
8. Using await with act
When testing asynchronous updates (like state changes after API calls), you might need to use act() in some cases to ensure all state updates are flushed before assertions. This is especially true when dealing with timers or useEffect.
import { act } from 'react-dom/test-utils';
await act(async () => {
jest.advanceTimersByTime(2000); // Simulate 2 seconds passing
});
