Technology · React

React Testing

Test React components with Jest and React Testing Library to verify user behavior.

TL;DR
  1. 01Use React Testing Library to render components and query the DOM.
  2. 02Simulate user actions with userEvent, not the lower-level fireEvent.
  3. 03Query by role and visible text, not by class names or test IDs.

Basic Test Setup

  • Create a test file next to the component.
    // Button.test.js
    import { render, screen } from '@testing-library/react';
    import Button from './Button';
    
    test('renders a button', () => {
      render(<Button label="Click me" />);
      expect(screen.getByRole('button')).toBeInTheDocument();
    });
  • Run tests with the npm test command.
    npm test              # watch mode
    npm test -- --ci      # run once for CI
    npm test -- --coverage # generate coverage report
  • Install the necessary packages for a new project.
    npm install --save-dev @testing-library/react @testing-library/user-event @testing-library/jest-dom
  • Import jest-dom matchers to enable toBeInTheDocument and similar.
    // setupTests.js — imported in jest config
    import '@testing-library/jest-dom';
  • Use describe to group related tests.
    describe("Button", () => {
      test("renders the label", () => { /* ... */ });
      test("calls onClick when pressed", () => { /* ... */ });
      test("is disabled when prop is set", () => { /* ... */ });
    });

Rendering Components

  • Render a component and query the result with screen.
    import { render, screen } from '@testing-library/react';
    
    test('renders component', () => {
      render(<MyComponent />);
      expect(screen.getByText('Expected text')).toBeInTheDocument();
    });
  • Use semantic queries to find elements the way users see them.
    screen.getByRole('button', { name: 'Submit' });
    screen.getByText('Label');
    screen.getByPlaceholderText('Enter name');
    screen.getByLabelText('Email');
  • Use getBy for elements that must exist, queryBy for optional ones.
    screen.getByRole('button');         // throws if missing
    screen.queryByText('Error');        // returns null if missing
    await screen.findByText('Loaded');  // waits for element to appear
  • Wrap providers around components that need context.
    test('shows user name from context', () => {
      render(
        <UserContext.Provider value={{ user: { name: 'Alice' } }}>
          <Greeting />
        </UserContext.Provider>
      );
      expect(screen.getByText('Hello, Alice')).toBeInTheDocument();
    });
  • Use rerender to test how a component responds to prop changes.
    const { rerender } = render(<Badge count={0} />);
    expect(screen.getByText('0')).toBeInTheDocument();
    
    rerender(<Badge count={5} />);
    expect(screen.getByText('5')).toBeInTheDocument();

User Interactions

  • Simulate user clicks with userEvent.click.
    import userEvent from '@testing-library/user-event';
    
    test('handles click', async () => {
      const user = userEvent.setup();
      render(<Counter />);
      
      const button = screen.getByRole('button', { name: 'Increment' });
      await user.click(button);
      
      expect(screen.getByText('Count: 1')).toBeInTheDocument();
    });
  • Type into inputs with userEvent.type.
    test('updates input on type', async () => {
      const user = userEvent.setup();
      render(<SearchBox />);
      
      const input = screen.getByPlaceholderText('Search...');
      await user.type(input, 'React hooks');
      
      expect(input).toHaveValue('React hooks');
    });
  • Submit a form to test validation and submission handlers.
    test('submits the form', async () => {
      const user = userEvent.setup();
      const onSubmit = jest.fn();
      render(<LoginForm onSubmit={onSubmit} />);
      
      await user.type(screen.getByLabelText('Email'), 'alice@example.com');
      await user.type(screen.getByLabelText('Password'), 'secret123');
      await user.click(screen.getByRole('button', { name: 'Log in' }));
      
      expect(onSubmit).toHaveBeenCalledWith({ email: 'alice@example.com' });
    });
  • Check a checkbox or select from a dropdown.
    await user.click(screen.getByRole('checkbox', { name: 'Accept terms' }));
    expect(screen.getByRole('checkbox')).toBeChecked();
    
    await user.selectOptions(screen.getByRole('combobox'), 'Option B');
  • Use keyboard navigation to test accessibility interactions.
    await user.keyboard('{Tab}');                // move focus
    await user.keyboard('{Enter}');              // activate focused element
    await user.keyboard('{Escape}');             // close modal

Async Testing

  • Wait for elements to appear with waitFor.
    import { waitFor } from '@testing-library/react';
    
    test('loads data', async () => {
      render(<DataComponent />);
      
      await waitFor(() => {
        expect(screen.getByText('Data loaded')).toBeInTheDocument();
      });
    });
  • Use findBy queries as a shorter alternative to waitFor.
    // findBy automatically waits — no waitFor needed
    const item = await screen.findByText('Loaded item');
    expect(item).toBeInTheDocument();
  • Mock fetch to control async responses in tests.
    global.fetch = jest.fn(() =>
      Promise.resolve({
        json: () => Promise.resolve([{ id: 1, name: "Alice" }])
      })
    );
    
    test('renders fetched data', async () => {
      render(<UserList />);
      expect(await screen.findByText('Alice')).toBeInTheDocument();
    });
  • Test loading and error states for async components.
    test('shows loading then data', async () => {
      render(<DataComponent />);
      expect(screen.getByText('Loading...')).toBeInTheDocument();
      expect(await screen.findByText('Alice')).toBeInTheDocument();
    });
  • Use MSW (Mock Service Worker) for realistic API mocking.
    import { http, HttpResponse } from 'msw';
    import { server } from './mocks/server';
    
    test('handles API error', async () => {
      server.use(http.get('/api/users', () => HttpResponse.error()));
      render(<UserList />);
      expect(await screen.findByText('Failed to load')).toBeInTheDocument();
    });

Mocking

  • Mock a function with jest.fn to track calls and set return values.
    const mockOnClick = jest.fn();
    render(<Button onClick={mockOnClick}>Click</Button>);
    await userEvent.setup().click(screen.getByRole('button'));
    
    expect(mockOnClick).toHaveBeenCalledTimes(1);
  • Mock an entire module with jest.mock.
    jest.mock('./api', () => ({
      fetchUser: jest.fn(() => Promise.resolve({ name: 'Alice' }))
    }));
  • Spy on a module method without replacing it entirely.
    import * as api from './api';
    jest.spyOn(api, 'fetchUser').mockResolvedValue({ name: 'Bob' });
  • Reset mocks between tests to avoid leaking state.
    afterEach(() => {
      jest.clearAllMocks(); // resets call counts and instances
    });
  • Mock timers to test debounced or delayed behavior.
    jest.useFakeTimers();
    render(<Debounced />);
    await userEvent.setup().type(input, 'hello');
    jest.advanceTimersByTime(300); // skip the debounce delay
    expect(screen.getByText('hello')).toBeInTheDocument();

Tip: Test user behavior, not implementation — query by role or visible text, not by test IDs.

Warning: Avoid testing implementation details like internal state — test what users see and interact with.

React Testing Best PracticesReact useContext Advanced Patterns