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.
Tips
- 01Test user behavior, not implementation — query by role or visible text, not by test IDs.
Warnings
- 01Avoid testing implementation details like internal state — test what users see and interact with.
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();
FAQ
userEvent simulates real browser interactions more accurately — it triggers the full sequence of events (pointerdown, focus, input, click, etc.) that a real user would cause, while fireEvent dispatches a single synthetic event. Prefer userEvent for most interaction tests.
Use findBy queries (e.g., findByRole, findByText) which return a promise and automatically wait for the element to appear. Alternatively, wrap your assertion in waitFor(() => expect(...)) for more complex async scenarios.
Mock the fetch or axios module using jest.mock, or intercept requests with a library like msw (Mock Service Worker). Return controlled responses so your test verifies how the component handles success and error states without making real network calls.
Querying by test ID only works in tests and tells you nothing about what users actually experience. Prefer getByRole or getByText, which validate accessibility and visible content — they catch regressions that matter to users, not just DOM structure.
Pass a jest.fn() as the onSubmit prop, use userEvent.type to fill in fields, then userEvent.click the submit button. Assert with expect(mockHandler).toHaveBeenCalledWith(expect.objectContaining({ fieldName: 'value' })).