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