Technology · React
React Testing
Test React components with Jest and React Testing Library to verify user behavior.
TL;DR
- 01Use React Testing Library to render components and query the DOM.
- 02Simulate user actions with userEvent, not the lower-level fireEvent.
- 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.