Skip to main content
Testing React components ensures they work correctly and prevents regressions. This guide covers common testing patterns using React Testing Library and Jest.

Testing Philosophy

Test User Behavior

Test components the way users interact with them, not implementation details.

Avoid Testing Internals

Don’t test state variables or internal functions directly. Test the output.

Use Real DOM

React Testing Library uses real DOM nodes for more realistic tests.

Accessibility First

Query elements by accessible attributes like labels and roles.

Common Testing Patterns

Testing Asynchronous Updates

Many React components update asynchronously, especially when using libraries like React DnD or data fetching hooks. Here’s how to handle async updates:
If you see warnings about wrapping tests in act(...), it usually means:
  • The component updated after the test completed
  • There are pending async updates
  • You need to wait for updates to finish
The solution is to use waitFor() from React Testing Library.

Example: Testing a Drag and Drop Component

Here’s a real example of testing a component that updates asynchronously:
import React from 'react';
import { useDrag } from 'react-dnd';

const Card = ({
  card: {
    id,
    title
  }
}) => {
  const [style, drag] = useDrag({
    item: { id, type: 'card' },
    collect: monitor => ({
      opacity: monitor.isDragging() ? 0 : 1
    })
  });

  return (
    <li className="card" id={id} ref={drag} style={style}>
      {title}
    </li>
  );
};
import React from 'react';
import { fireEvent } from '@testing-library/react';
import Card from './components/Card';
import renderDndConnected from './test_utils/renderDndConnected';

describe('<Card/>', () => {
  let card;

  beforeEach(() => {
    const utils = renderDndConnected(
      <Card card={{ id: '1', title: 'Card' }} />
    );
    card = utils.container.querySelector('.card');
  });

  it('initial opacity is 1', () => {
    expect(card.style.opacity).toEqual('1');
  });

  describe('when drag starts', () => {
    beforeEach(() => {
      fireEvent.dragStart(card);
    });

    // This test fails with act(...) warning
    it('opacity is 0', () => {
      expect(card.style.opacity).toEqual('0');
    });
  });
});
This test fails because the dragStart event doesn’t immediately update the component’s style. The collect function takes time to run.

Key Testing Concepts

Using waitFor()

waitFor() is essential for testing components that update asynchronously:
import { waitFor } from '@testing-library/react';

// Wait for an element to appear
await waitFor(() => {
  expect(screen.getByText('Loaded')).toBeInTheDocument();
});

// Wait for a condition
await waitFor(() => {
  expect(mockCallback).toHaveBeenCalled();
});

// Wait for style changes
await waitFor(() => {
  expect(element.style.opacity).toEqual('0');
});
waitFor() repeatedly checks the assertion until it passes or times out. This is perfect for async updates.

Testing User Interactions

Test components by simulating user actions:
import { render, fireEvent, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

// Using fireEvent (synchronous)
fireEvent.click(button);
fireEvent.change(input, { target: { value: 'new value' } });
fireEvent.dragStart(element);

// Using userEvent (more realistic, async)
await userEvent.click(button);
await userEvent.type(input, 'new value');

Testing Redux Connected Components

When testing components connected to Redux:
import { render } from '@testing-library/react';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import rootReducer from './reducers';

const renderWithRedux = (
  component,
  { initialState, store = createStore(rootReducer, initialState) } = {}
) => {
  return {
    ...render(<Provider store={store}>{component}</Provider>),
    store
  };
};

// Usage
const { getByText, store } = renderWithRedux(<MyComponent />, {
  initialState: { user: { name: 'John' } }
});

Testing Portals

Components that render to portals (like modals) need special handling:
import { render } from '@testing-library/react';

// Ensure the portal container exists
beforeEach(() => {
  const portalRoot = document.createElement('div');
  portalRoot.setAttribute('id', 'portal-root');
  document.body.appendChild(portalRoot);
});

afterEach(() => {
  document.body.removeChild(document.getElementById('portal-root'));
});

it('renders modal in portal', () => {
  const { getByText } = render(<Modal isOpen={true} />);
  expect(getByText('Modal content')).toBeInTheDocument();
});

Common Test Patterns

Testing Hooks

Test custom hooks using the renderHook utility:
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';

it('increments counter', () => {
  const { result } = renderHook(() => useCounter());
  
  act(() => {
    result.current.increment();
  });
  
  expect(result.current.count).toBe(1);
});

Testing Forms

Test form submissions and validation:
import { render, fireEvent, screen } from '@testing-library/react';

it('submits form with valid data', async () => {
  const handleSubmit = jest.fn();
  render(<MyForm onSubmit={handleSubmit} />);
  
  await userEvent.type(screen.getByLabelText('Email'), '[email protected]');
  await userEvent.type(screen.getByLabelText('Password'), 'password123');
  
  fireEvent.submit(screen.getByRole('form'));
  
  await waitFor(() => {
    expect(handleSubmit).toHaveBeenCalledWith({
      email: '[email protected]',
      password: 'password123'
    });
  });
});

Testing Error States

Test error handling and error boundaries:
import { render, screen } from '@testing-library/react';

it('displays error message on fetch failure', async () => {
  global.fetch = jest.fn(() => Promise.reject('API error'));
  
  render(<DataFetchingComponent />);
  
  await waitFor(() => {
    expect(screen.getByText('Error loading data')).toBeInTheDocument();
  });
});

Best Practices

Query Priority

Prefer queries in this order:
  1. getByRole
  2. getByLabelText
  3. getByPlaceholderText
  4. getByText
  5. getByTestId (last resort)

Async Utilities

Use waitFor, findBy*, and waitForElementToBeRemoved for async operations.

User Events

Prefer @testing-library/user-event over fireEvent for more realistic interactions.

Cleanup

React Testing Library automatically cleans up, but clean up mocks and timers manually.

Summary: Key Takeaways

1

Handle Async Updates

Use waitFor() when testing components that update asynchronously. This solves most act(...) warnings.
2

Test User Behavior

Focus on testing what users see and do, not implementation details.
3

Setup Test Environment

Ensure you have the latest testing library versions and proper test environment configuration.
4

Mock External Dependencies

Mock API calls, timers, and external libraries for reliable tests.
Common issues:
  • act(...) warnings usually mean async updates weren’t awaited
  • MutationObserver is not a constructor requires --env=jsdom-fourteen
  • Missing cleanup can cause test interference

Build docs developers (and LLMs) love