How do you test React applications?
TL;DR
A common stack is Jest or Vitest as the test runner with React Testing Library, which encourages testing observable behavior instead of implementation details. Drive realistic interactions with @testing-library/user-event, mock network calls with a tool such as MSW, and cover critical flows in a real browser with Playwright or Cypress. Use async queries (findBy*, waitFor) for UI that appears after asynchronous client updates. Test Server Components through the integration facilities of the framework and bundler that implement them.
How do you test React applications?
A balanced React test suite checks user-visible behavior at component boundaries and reserves browser tests for critical integrated flows.
Unit testing
Unit testing covers individual components in isolation. Jest and Vitest are the two dominant runners — Vitest is the natural choice for Vite-based projects and has largely caught up with Jest in features. Both pair with React Testing Library, which renders components and queries the DOM the way a user would.
Add @testing-library/jest-dom once in your setup file (e.g. setupTests.ts) so its matchers are registered globally — the older @testing-library/jest-dom/extend-expect import path is no longer needed:
// setupTests.tsimport '@testing-library/jest-dom';
// MyComponent.test.tsximport { render, screen } from '@testing-library/react';import MyComponent from './MyComponent';test('renders the component with the correct text', () => {render(<MyComponent />);expect(screen.getByText('Hello, World!')).toBeInTheDocument();});
Integration testing
Integration tests exercise multiple components together. Prefer @testing-library/user-event over fireEvent — it simulates real user interactions (focus, hover, typing) much more faithfully and returns a promise, so you await the action.
import { render, screen } from '@testing-library/react';import userEvent from '@testing-library/user-event';import ParentComponent from './ParentComponent';test('updates child component when parent state changes', async () => {const user = userEvent.setup();render(<ParentComponent />);await user.click(screen.getByRole('button', { name: 'Update Child' }));expect(await screen.findByText('Child Updated')).toBeInTheDocument();});
Note findByText (async) instead of getByText for assertions on UI that appears after an update — findBy* retries until the element appears or times out, and is the standard tool for anything asynchronous. For more complex polling, use waitFor.
Testing custom hooks
Use renderHook from @testing-library/react to test hooks in isolation:
import { renderHook, act } from '@testing-library/react';import { useCounter } from './useCounter';test('increments the counter', () => {const { result } = renderHook(() => useCounter());act(() => result.current.increment());expect(result.current.count).toBe(1);});
Mocking network requests with MSW
Mock Service Worker (MSW) intercepts requests at the network layer rather than stubbing fetch, so the same handlers can be reused in unit tests, Storybook, and the dev server. It is a widely used option for API mocking in React tests.
import { http, HttpResponse } from 'msw';import { setupServer } from 'msw/node';const server = setupServer(http.get('/api/user', () => HttpResponse.json({ name: 'Ada' })),);beforeAll(() => server.listen());afterEach(() => server.resetHandlers());afterAll(() => server.close());
Testing asynchronous UI and Server Components
For client UI that updates after a request, timer, or transition, trigger the behavior and await the visible result with findBy* or waitFor. Do not make the Client Component itself an async function; client-side suspension should use a supported Suspense-enabled data source.
React Server Components rely on a framework or bundler to produce and consume their serialized payload. Calling a Server Component as an ordinary async function does not test that pipeline. Prefer the framework's integration or end-to-end testing setup, and unit-test pure data helpers separately. Keep network calls controlled at an appropriate boundary, for example with MSW or a stubbed data module.
test('renders user data once loaded', async () => {render(<UserProfile id="42" />);expect(await screen.findByText('Ada')).toBeInTheDocument();});
End-to-end testing
End-to-end tests drive the whole application in a real browser. Playwright is a common choice for new projects and includes parallel execution, multi-browser support, auto-waiting, and a trace viewer. Cypress is also widely used and is a fine option, especially in an existing codebase.
// Playwrightimport { test, expect } from '@playwright/test';test('user can log in', async ({ page }) => {await page.goto('/login');await page.getByLabel('Username').fill('user');await page.getByLabel('Password').fill('password');await page.getByRole('button', { name: 'Sign in' }).click();await expect(page).toHaveURL(/\/dashboard/);});
Snapshot testing
Snapshot testing captures the rendered output of a component and compares it to a saved snapshot on subsequent runs. With React 19, react-test-renderer is deprecated — render with React Testing Library instead and snapshot the resulting markup:
import { render } from '@testing-library/react';import MyComponent from './MyComponent';test('matches the snapshot', () => {const { asFragment } = render(<MyComponent />);expect(asFragment()).toMatchSnapshot();});
Use snapshot tests sparingly — they're easy to update reflexively, which can let regressions slip through. Reserve them for stable presentational output.
Further reading
- Jest documentation
- Vitest documentation
- React Testing Library documentation
user-eventdocumentationrenderHookAPI- Mock Service Worker (MSW)
- Playwright documentation
- Cypress documentation
- Snapshot testing with Jest