Testing React components the way users interact with them using React Testing Library's query priority hierarchy, userEvent for realistic interactions, and the philosophy of testing behavior over implementation details.
Test what users see and do, not implementation details. This makes tests resilient to refactoring -- if the component looks and behaves the same, tests pass regardless of internal changes.
Prefer getByRole (accessibility-first), then getByLabelText, getByText, and getByTestId as a last resort. This priority ensures tests mirror how users and assistive technology find elements.
userEvent simulates complete interaction sequences (focus, pointer, mouse, click) like a real browser, while fireEvent only dispatches individual DOM events.
getBy throws if not found (elements that must exist), queryBy returns null (testing absence), and findBy returns a promise (waiting for async elements to appear).
React Testing Library (RTL) is the standard library for testing React components. Its core philosophy -- 'The more your tests resemble the way your software is used, the more confidence they can give you' -- drives a fundamentally different approach from enzyme-style testing that inspects component internals.
RTL encourages testing components from the user's perspective. Instead of checking internal state, prop values, or instance methods, you verify what the user sees and how the component responds to interactions. This makes tests resilient to refactoring -- you can completely rewrite a component's internals, and if it still looks and behaves the same, tests pass.
RTL provides multiple ways to find elements, ordered by preference:
screen.getByRole('button', { name: 'Submit' });
screen.getByRole('heading', { level: 2 });screen.getByLabelText('Email address');getByPlaceholderText: When no label exists (not ideal, but realistic).
getByText: For non-interactive elements where the text content identifies the element.
screen.getByText('Welcome back!');getByDisplayValue: For inputs with current values.
getByAltText: For images.
getByTitle: For elements with title attributes.
getByTestId: Last resort. Use data-testid attributes when no semantic query works. This is an escape hatch, not the default.
Each query comes in three variants:
getBy: Throws if element not found (synchronous, for elements that should exist)queryBy: Returns null if not found (use for asserting absence: expect(queryByRole('alert')).not.toBeInTheDocument())findBy: Returns a promise, waits for element to appear (use for async rendering)userEvent from @testing-library/user-event simulates full user interaction sequences, not just individual DOM events:
import userEvent from '@testing-library/user-event';
const user = userEvent.setup();
await user.click(screen.getByRole('button', { name: 'Submit' }));
await user.type(screen.getByRole('textbox'), 'hello');userEvent.click() fires focus, pointerdown, mousedown, pointerup, mouseup, and click events in the correct order, just like a real browser click. fireEvent.click() only dispatches the click event. Always prefer userEvent for realistic interaction testing.
Waiting for async updates:
await waitFor(() => {
expect(screen.getByText('Loaded')).toBeInTheDocument();
});Testing absence:
expect(screen.queryByRole('alert')).not.toBeInTheDocument();Testing within a container:
const nav = screen.getByRole('navigation');
within(nav).getByRole('link', { name: 'Home' });The @testing-library/jest-dom package adds DOM-specific matchers:
toBeInTheDocument(): Element exists in the DOMtoBeVisible(): Element is visible to the usertoBeDisabled() / toBeEnabled(): Form element statetoHaveTextContent(text): Element contains specific texttoHaveAttribute(attr, value): Element has an HTML attributetoHaveClass(className): Element has a CSS classcontainer.querySelector instead of semantic queriesact() manually (RTL handles this automatically)Fun Fact
React Testing Library was created by Kent C. Dodds in 2018 as a response to Enzyme, which encouraged testing implementation details. RTL's philosophy proved so popular that it spawned an entire family: Testing Library for Vue, Angular, Svelte, and even plain DOM, all sharing the same query API.