How do you write unit tests in React using Jest and React Testing Library?
Jest is the test runner; React Testing Library (RTL) is the rendering + querying layer. Philosophy: test BEHAVIOR (what users see and do), not implementation. Use `render`, query by role/text (`getByRole`, `findByText`), simulate with `userEvent`, assert with `expect`. Mock network with msw, not fetch stubs.
Modern React testing centers on user-visible behavior, not internals.
Setup
npm i -D jest @testing-library/react @testing-library/user-event @testing-library/jest-dom jsdomjest.config.ts:
export default {
testEnvironment: 'jsdom',
setupFilesAfterEach: ['<rootDir>/test/setup.ts'],
};test/setup.ts:
import '@testing-library/jest-dom';A first test
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Counter from './Counter';
test('increments on click', async () => {
render(<Counter />);
expect(screen.getByText(/count: 0/i)).toBeInTheDocument();
await userEvent.click(screen.getByRole('button', { name: /increment/i }));
expect(screen.getByText(/count: 1/i)).toBeInTheDocument();
});Query priority (RTL's golden rule)
getByRole— accessible roles (button, link, heading). Forces accessible markup.getByLabelText— form fields by their label.getByPlaceholderText— second choice for inputs.getByText— visible text.getByTestId— escape hatch only.
If you have to reach for testId, ask whether your component is accessible at all.
Async behavior
test('loads user', async () => {
render(<UserCard id="1" />);
expect(await screen.findByText('Alice')).toBeInTheDocument();
});findBy* waits up to 1 second; waitFor for arbitrary assertions.
Mocking network — use msw, not fetch stubs
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
const server = setupServer(
http.get('/api/user/1', () => HttpResponse.json({ name: 'Alice' })),
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());msw intercepts at the network layer so your component code runs the real fetch path.
Testing custom hooks
import { renderHook, act } from '@testing-library/react';
test('useCounter', () => {
const { result } = renderHook(() => useCounter(0));
act(() => result.current.inc());
expect(result.current.count).toBe(1);
});What NOT to test
- Implementation details: state names, function references, internal effects.
- Library code: don't test that React re-renders.
- Style: visual tests (Playwright/Chromatic) handle this.
Coverage strategy
Pyramid: many small unit tests for utility functions, fewer component tests for behavior, a handful of integration/E2E tests for critical journeys.
Common patterns
- act() warnings: usually mean an async update happened outside a wait. Use
findByor wrap inwaitFor. - Snapshot tests: useful sparingly — they break on every layout tweak and tend to be rubber-stamped.
- userEvent vs fireEvent: prefer
userEvent— it simulates the full event sequence (focus, mousedown, click, etc.) that real users trigger.
Follow-up questions
- •Why is queryByRole preferred over queryByTestId?
- •When would you use waitFor vs findBy?
- •How do you test components that consume context?
Common mistakes
- •Testing internal state: `expect(wrapper.state().count)` is Enzyme-era and fragile.
- •Reaching for testId on everything — usually means the component isn't accessible.
- •Mocking fetch directly when msw intercepts at the network layer with less code.
Performance considerations
- •Jest with jsdom is slow at startup. Vitest is significantly faster if you control the stack. Parallelize with --maxWorkers; isolate global setup; avoid heavy beforeEach.
Edge cases
- •Portals: RTL queries the whole document by default, so portaled content is reachable.
- •Async + StrictMode dev double-mount can cause spurious cleanup warnings — make effects idempotent.
- •Timers: use `jest.useFakeTimers()` and `vi.advanceTimersByTime()` for debounce/throttle tests.
Real-world examples
- •Most React open-source libraries (TanStack Query, React Hook Form) use RTL + msw. The pattern is standard enough that 'unit test in React' essentially means 'RTL test' in 2025.