React Asynchronous Testing
Modern React applications often rely on asynchronous operations, such as API calls, timeouts, or promises. Testing components that perform async actions requires special handling to ensure tests wait for updates before asserting results.
Why Asynchronous Testing is Important
- Ensures reliability – Prevents tests from passing before async code completes
- Simulates real user interactions – Like fetching data, waiting for UI updates
- Covers edge cases – Errors, delays, and loading states
Async Component Example
Suppose we have a component that fetches user data:
// UserProfile.jsx
import React, { useEffect, useState } from "react";
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`https://jsonplaceholder.typicode.com/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
}, [userId]);
if (!user) return <p>Loading...</p>;
return <div>{user.name}</div>;
}
export default UserProfile;
Testing Async Components with Jest and React Testing Library
Step 1: Mock the Fetch API
// UserProfile.test.jsx
import React from "react";
import { render, screen, waitFor } from "@testing-library/react";
import UserProfile from "./UserProfile";
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ name: "Jane Doe" }),
})
);
Step 2: Write Async Test Using waitFor
test("renders user name after API call", async () => {
render(<UserProfile userId={1} />);
// Wait for the async content to appear
const userName = await waitFor(() => screen.getByText("Jane Doe"));
expect(userName).toBeInTheDocument();
expect(fetch).toHaveBeenCalledTimes(1);
});
waitFor()waits until the callback stops throwing errors (useful for async updates)- Ensures component re-renders after async data
- Avoids flaky tests
Using findBy Queries
React Testing Library provides async-friendly queries like findByText, findByRole:
test("renders user name using findByText", async () => {
render(<UserProfile userId={1} />);
const userName = await screen.findByText("Jane Doe");
expect(userName).toBeInTheDocument();
});
- Automatically waits for the element to appear
- Cleaner syntax compared to
waitFor
Testing Async Events
You can also test components that use timeouts or delayed actions:
function DelayedMessage() {
const [message, setMessage] = React.useState("");
React.useEffect(() => {
setTimeout(() => {
setMessage("Hello after delay");
}, 1000);
}, []);
return <div>{message}</div>;
}
// Test
test("renders delayed message", async () => {
render(<DelayedMessage />);
const msg = await screen.findByText("Hello after delay");
expect(msg).toBeInTheDocument();
});
findByTextwaits for the message to appear- Handles setTimeout and other asynchronous updates
Testing Async Error Scenarios
global.fetch = jest.fn(() => Promise.reject("API error"));
test("handles API error", async () => {
render(<UserProfile userId={1} />);
// Custom error handling logic can be asserted here
});
- Allows testing error states and fallback UI
