Skip to content

Testing

The dominant approach for testing React components is React Testing Library (RTL), usually run on Vitest (or Jest). Its guiding principle:

The more your tests resemble the way your software is used, the more confidence they give you.

That means you don’t test internal state, prop names, or which hooks a component calls. You render the component, interact with it the way a user would (click, type), and assert on what the user can see. When you refactor the internals — swap useState for useReducer, extract a hook — behavior-based tests keep passing, because the behavior didn’t change. Implementation-based tests would break for no real reason.

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { expect, test } from 'vitest';
import Counter from './Counter';
test('increments when the button is clicked', async () => {
const user = userEvent.setup();
render(<Counter />);
// Query the way a user finds things: by role and accessible name.
const button = screen.getByRole('button', { name: /count: 0/i });
await user.click(button);
// Assert on what the user sees, not on internal state.
expect(screen.getByRole('button', { name: /count: 1/i })).toBeInTheDocument();
});

The important habits:

  • Query by role/label/text, in that priority — getByRole is best because it mirrors how assistive tech (and users) find elements, which also nudges you toward accessible markup.
  • Use userEvent, not raw fireEvent, for interactions — it simulates real user actions (focus, key sequences) more faithfully.
  • Assert on the rendered output (toBeInTheDocument, visible text), never on component.state.
LevelToolWhat it covers
UnitVitestPure functions, custom hooks (renderHook), reducers
ComponentRTL + VitestA component’s behavior in isolation (most of your tests)
End-to-endPlaywright / CypressReal browser, full user flows across pages
flowchart TB
  e2e["End-to-end (few)
Playwright — real browser, whole flows"]
  comp["Component (many)
RTL + Vitest — behavior in isolation"]
  unit["Unit (some)
Vitest — pure logic, hooks, reducers"]
  e2e --> comp --> unit
Where each kind of test earns its keep

Most of your effort lives in the middle — RTL component tests — because that’s where React logic actually is. A few end-to-end tests cover the critical happy paths across the real app.

What is the guiding principle of React Testing Library?
Which query is preferred for finding an element in RTL?
Why prefer behavior-based tests over implementation-based ones?
Where do most of your React tests live in the pyramid?