React Testing Fundamentals
Write confident, accessible component tests using React Testing Library, Vitest, and userEvent simulation.
TL;DR
- Test user interface components from the end-user
perspective. - Locate interactive accessible elements reliably using standard
screen.getByRoleselectors. - Simulate authentic user clicks and keyboard typing with
@testing-library/user-event.
Rendering and Screen Queries
Render ComponentMount component into virtual jsdom container.
import { render } from '@testing-library/react';
import { screen } from '@testing-library/react';
test('renders greeting', () => {
render(<Greeting name="Sam" />);
const el = screen.getByText(/hello sam/i);
expect(el).toBeInTheDocument();
});Query by Accessible RoleFind buttons, inputs, and headings by role.
const btn = screen.getByRole('button', {
name: /submit/i,
});Assert Element AbsenceUse queryBy to confirm element does not exist.
expect(screen.queryByRole('dialog')).toBeNull();Accessible Query Hierarchy
1. Queries Accessible to EveryonePriority 1: getByRole, getByLabelText, getByText.
screen.getByRole('button', { name: 'Save' });
screen.getByLabelText('Email address');2. Semantic QueriesPriority 2: getByAltText for images, getByTitle.
screen.getByAltText('Company logo');3. Test IDs Escape HatchPriority 3: getByTestId only when semantics fail.
screen.getByTestId('custom-canvas');User Event Simulation
User Event SetupInitialize user session before component render.
const user = userEvent.setup();
render(<Counter />);
await user.click(screen.getByRole('button'));Simulate Typing InputType characters realistically into text field.
const input = screen.getByRole('textbox');
await user.type(input, 'Hello World');
expect(input).toHaveValue('Hello World');Keyboard Navigation EventSimulate Tab and Enter keyboard actions.
await user.tab();
await user.keyboard('{Enter}');Testing Assertions
Element Visibility MatchersAssert visibility and DOM presence.
expect(btn).toBeVisible();
expect(btn).toBeDisabled();CSS Class and Attribute MatchersAssert specific attributes on elements.
expect(link).toHaveAttribute('href', '/login');Form Value MatchersAssert checkbox and input values.
expect(checkbox).toBeChecked();Tips
- Always use
screen.getByRoleas your primary query selector because it ensures your components are fully accessible to screen readers. - Always initialize
userEvent.setup()before rendering components to ensure realistic browser event dispatch sequences and state synchronization.
Warnings
- Avoid testing internal component state or private methods; tests should only interact with what users can
seeand touch. - Do not use
fireEventfor user interactions; preferuserEventbecause it dispatches realistic browser events faithfully.
In Practice
Write a complete Vitest and React Testing Library test asserting button rendering, click simulation, and DOM update.
- Initialize userEvent session before running tests.
- Render counter component into virtual DOM environment.
- Query increment button using accessible getByRole selector.
- Simulate authentic user click and assert updated text output.
import { render } from '@testing-library/react';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Counter } from './Counter';
test('increments count on click', async () => {
const user = userEvent.setup();
render(<Counter />);
const btn = screen.getByRole('button', {
name: /count: 0/i,
});
await user.click(btn);
expect(
screen.getByRole('button', { name: /count: 1/i })
).toBeInTheDocument();
});FAQ
Testing implementation details (like state names or internal functions) causes tests to break during harmless refactors without catching actual user-facing bugs.
getBy throws if not found, queryBy returns null (ideal for asserting absence), and findBy returns a promise that waits for async elements to appear.
fireEvent dispatches raw DOM events synthetically, while userEvent simulates complete realistic interactions including focus, hover, and key down/up sequences.