How to Test React Suspense
Testing React Suspense is essential for ensuring that your application handles asynchronous operations, such as code-splitting and data fetching, without breaking the user experience. This article provides a practical guide on how to write unit tests for React Suspense components using Jest and React Testing Library. You will learn how to verify fallback loading states, test lazy-loaded components, and handle resolved asynchronous states in a predictable manner.
Understanding the Challenge
React Suspense works by catching promises thrown by its child components. While a promise is pending, Suspense renders a fallback UI (like a loading spinner). Once the promise resolves, it renders the actual component.
To test this behavior, your testing environment must simulate these asynchronous transitions. React Testing Library (RTL) paired with Jest is the industry standard for this task, as it allows you to easily query the DOM as it changes over time.
Testing Lazy-Loaded Components
The most common use case for Suspense is code-splitting with
React.lazy. Testing a lazy-loaded component requires you to
assert that the fallback is displayed first, and then the actual
component appears once the import promise resolves.
Here is how to test a lazy-loaded component:
import React, { Suspense } from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
// Lazy load the component
const LazyComponent = React.lazy(() => import('./MyComponent'));
describe('LazyComponent with Suspense', () => {
test('renders fallback first, then renders the lazy component', async () => {
render(
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
// 1. Verify that the fallback loading state is rendered immediately
expect(screen.getByText('Loading...')).toBeInTheDocument();
// 2. Wait for the lazy component to resolve and render its content
const resolvedElement = await screen.findByText('Loaded Content');
expect(resolvedElement).toBeInTheDocument();
// 3. Verify that the fallback is no longer in the DOM
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
});
});Using screen.findByText() is crucial here. It internally
uses waitFor to repeatedly query the DOM until the element
appears or the timeout is reached, which accommodates the asynchronous
nature of the dynamic import.
Testing Suspense Data Fetching
When using Suspense for data fetching (such as with libraries like Relay, Next.js, or SWR), the child component throws a promise when data is loading. To test this, you must mock your data-fetching API to return a promise that you can resolve manually within your test.
Below is an example of testing a component that fetches data using a Suspense-compatible resource:
import React, { Suspense } from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { fetchUserData } from './api';
import UserProfile from './UserProfile';
// Mock the API module
jest.mock('./api');
describe('UserProfile with Suspense Data Fetching', () => {
test('displays loading spinner and then user profile data', async () => {
let resolvePromise;
const promise = new Promise((resolve) => {
resolvePromise = resolve;
});
// Mock the API to return our controlled promise
fetchUserData.mockReturnValue({
read() {
if (resolvePromise) {
throw promise; // Suspend the component
}
return { name: 'John Doe' };
},
});
render(
<Suspense fallback={<div>Loading User...</div>}>
<UserProfile />
</Suspense>
);
// Verify fallback is active
expect(screen.getByText('Loading User...')).toBeInTheDocument();
// Resolve the promise to trigger the update
resolvePromise();
// Wait for the suspended component to render the resolved state
const username = await screen.findByText('John Doe');
expect(username).toBeInTheDocument();
});
});Best Practices for Testing Suspense
- Avoid manual timers: Do not use
setTimeoutor arbitrary delays to wait for Suspense to resolve. Always rely on utilities likefindByorwaitForfrom React Testing Library. - Mock network requests consistently: Use libraries like Mock Service Worker (MSW) or Jest mocks to ensure your network requests resolve reliably during testing.
- Test the Error Boundary: Suspense is often paired with Error Boundaries to catch loading failures. Ensure you write tests where the promise rejects, verifying that your error boundary fallback UI displays correctly.