React Testing Best Practices

Master advanced React testing including async assertions, Mock Service Worker (MSW), custom hooks, and Provider wrappers.

TL;DR

  1. Await asynchronous rendering transitions cleanly using modern findByRole queries.
  2. Intercept network requests reliably using Mock Service Worker.
  3. Test headless stateful logic in isolation using renderHook.

Asynchronous Testing

    findBy Query Resolution

    Await appearance of asynchronously fetched data.

    test('loads user name', async () => {
      render(<UserProfile id="1" />);
      const heading = await screen.findByRole('heading', {
        name: /alex johnson/i,
      });
      expect(heading).toBeInTheDocument();
    });
    waitFor Assertion Guard

    Wait until expectation passes without error.

    await waitFor(() => {
      expect(screen.queryByText(/loading/i)).toBeNull();
    });
    Avoid Act Warnings

    Awaiting findBy automatically flushes act transitions.

    // Clean: findBy awaits effect completion
    const card = await screen.findByText('Ready');

Mocking APIs with MSW

    Define MSW Handlers

    Intercept HTTP requests at network boundary.

    import { http, HttpResponse } from 'msw';
    
    export const handlers = [
      http.get('/api/user', () => {
        return HttpResponse.json({ name: 'Alex' });
      }),
    ];
    Server Lifecycle in Tests

    Configure MSW listener before and after test suite.

    beforeAll(() => server.listen());
    afterEach(() => server.resetHandlers());
    afterAll(() => server.close());
    Simulate Server Error Response

    Override handler per test to verify error UI.

    server.use(
      http.get('/api/user', () => {
        return new HttpResponse(null, { status: 500 });
      })
    );

Testing Custom Hooks with renderHook

    renderHook API

    Mount custom hook in headless test environment.

    import { renderHook } from '@testing-library/react';
    import { act } from '@testing-library/react';
    
    test('increments count', () => {
      const { result } = renderHook(() => useCounter());
      expect(result.current.count).toBe(0);
      act(() => result.current.inc());
      expect(result.current.count).toBe(1);
    });
    Hook with Initial Props

    Pass dynamic props into hook during test run.

    const { result, rerender } = renderHook(
      ({ step }) => useCounter(step),
      { initialProps: { step: 1 } }
    );
    rerender({ step: 5 });
    Async Hook Testing

    Await async state transitions inside custom hook.

    const { result } = renderHook(() => useAsyncData());
    await waitFor(() => {
      expect(result.current.isDone).toBe(true);
    });

Testing Context and Providers

    Custom Render Wrapper

    Build reusable render function wrapping all providers.

    function AllProviders({ children }: Props) {
      return (
        <QueryClientProvider client={testQueryClient}>
          <ThemeProvider>{children}</ThemeProvider>
        </QueryClientProvider>
      );
    }
    const customRender = (ui: any) =>
      render(ui, { wrapper: AllProviders });
    Consumer Test Isolation

    Test consumer component with mocked context value.

    render(
      <AuthContext.Provider value={{ user: 'TestUser' }}>
        <UserHeader />
      </AuthContext.Provider>
    );
    Provider Invariant Error Test

    Assert that hook throws error outside Provider.

    expect(() => renderHook(() => useAuth())).toThrow(
      'useAuth must be in AuthProvider'
    );

Tips

  1. Always prefer screen.findByRole over waitFor because findBy queries have built-in retry intervals and automatic timeout error reporting.
  2. Use Mock Service Worker (MSW) to intercept HTTP requests at the network layer rather than mocking fetch modules directly.

Warnings

  1. Avoid wrapping test assertions inside arbitrary sleep or timeout delays; always synchronize assertions using waitFor or findBy.
  2. Do not test third-party library internals; verify only the contracts and behaviors your own components consume and expose.

In Practice

FAQ