Skip to main content

Overview

Testing ensures your React components work correctly and helps prevent bugs. This section covers testing React components using React Testing Library and Jest.
React Testing Library encourages testing components from a user’s perspective, focusing on behavior rather than implementation details.

Setting Up Tests

Test Structure

Tests follow the Arrange-Act-Assert pattern:
  1. Arrange: Set up the component
  2. Act: Perform actions (if needed)
  3. Assert: Verify the expected outcome

Writing Your First Test

const Greeting = () => {
  return (
    <div>
      <h2>Hello World!</h2>
      <p>It's good to see you!</p>
    </div>
  );
};

export default Greeting;
1

Render the component

Use render() from React Testing Library to render your component in a test environment.
2

Query for elements

Use screen.getByText(), screen.getByRole(), or other query methods to find elements.
3

Assert expectations

Use Jest matchers like toBeInTheDocument() to verify the component behaves correctly.

Organizing Tests with Test Suites

Group related tests using describe() blocks:
import { render, screen } from '@testing-library/react';
import Greeting from './Greeting';

describe('Greeting component', () => {
  test('renders "Hello World" as a text', () => {
    render(<Greeting />);
    const helloWorldElement = screen.getByText('Hello World!');
    expect(helloWorldElement).toBeInTheDocument();
  });

  test('renders "good to see" you if the button was NOT clicked', () => {
    render(<Greeting />);
    const outputElement = screen.getByText('good to see you', { exact: false });
    expect(outputElement).toBeInTheDocument();
  });
});
Use describe() blocks to organize tests by component or feature, making test output more readable.

Testing User Interactions

Test user interactions using @testing-library/user-event:
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Greeting from './Greeting';

test('renders "Changed!" if the button was clicked', async () => {
  // Arrange
  render(<Greeting />);

  // Act
  const buttonElement = screen.getByRole('button');
  await userEvent.click(buttonElement);

  // Assert
  const outputElement = screen.getByText('Changed!');
  expect(outputElement).toBeInTheDocument();
});

test('does not render "good to see you" if the button was clicked', async () => {
  render(<Greeting />);
  
  const buttonElement = screen.getByRole('button');
  await userEvent.click(buttonElement);

  // Use queryByText when element shouldn't exist
  const outputElement = screen.queryByText('good to see you', { exact: false });
  expect(outputElement).toBeNull();
});

getByText

Throws an error if element is not found. Use when element must exist.

queryByText

Returns null if element is not found. Use when testing element absence.

Testing Asynchronous Code

Test components that fetch data using findBy queries:
import { useEffect, useState } from 'react';

const Async = () => {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    fetch('https://jsonplaceholder.typicode.com/posts')
      .then((response) => response.json())
      .then((data) => {
        setPosts(data);
      });
  }, []);

  return (
    <div>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    </div>
  );
};

export default Async;
Use findBy queries (not getBy) when testing async operations. They return a Promise and wait for elements to appear.

Working with Mocks

Mock external dependencies to avoid real API calls:
import { render, screen } from '@testing-library/react';
import Async from './Async';

describe('Async component', () => {
  test('renders posts if request succeeds', async () => {
    // Mock the fetch function
    window.fetch = jest.fn();
    window.fetch.mockResolvedValueOnce({
      json: async () => [{ id: 'p1', title: 'First post' }],
    });
    
    render(<Async />);

    const listItemElements = await screen.findAllByRole('listitem');
    expect(listItemElements).not.toHaveLength(0);
  });
});
Mocking prevents real network requests during tests, making tests:
  • Faster and more reliable
  • Independent of external services
  • Deterministic with controlled data
  • Mock API calls with jest.fn()
  • Mock resolved promises with mockResolvedValueOnce()
  • Mock rejected promises with mockRejectedValueOnce()
  • Mock modules with jest.mock()

Query Methods

getBy

SynchronousThrows error if not found. Use for elements that should exist immediately.

queryBy

SynchronousReturns null if not found. Use to test element absence.

findBy

AsynchronousWaits and returns Promise. Use for async elements.

Best Practices

Focus on what users see and do, not internal component details.
// Good: Test user-visible behavior
const button = screen.getByRole('button', { name: /add todo/i });

// Bad: Test implementation details
expect(wrapper.state().todos).toHaveLength(1);
Prefer queries that reflect how users interact with your app.
// Best: By role (most accessible)
screen.getByRole('button', { name: /submit/i });

// Good: By label text
screen.getByLabelText(/username/i);

// Okay: By text
screen.getByText(/hello world/i);

// Last resort: By test ID
screen.getByTestId('submit-button');
Each test should be independent and not rely on other tests.
describe('TodoList', () => {
  test('adds a new todo', () => {
    // Setup and test in isolation
  });
  
  test('removes a todo', () => {
    // Don't depend on previous test
  });
});

Running Tests

# Run all tests
npm test

# Run tests in watch mode
npm test -- --watch

# Run with coverage
npm test -- --coverage

Next Steps

TypeScript

Learn how to use TypeScript with React for type safety

Advanced Patterns

Explore advanced testing patterns and custom hooks testing

Build docs developers (and LLMs) love