Testing React Components
IntermediateTesting Library's whole philosophy is to interact with a component the way a user does. Query by role and text, not by class name or component internals.
Overview
The temptation is to test that a component has three divs and that state is set to loading. Those tests break on every refactor and catch nothing users care about. Testing Library pushes you towards the opposite: find elements the way a person or a screen reader would, act through real events, and assert on what appears. The side effect is worth noticing — a component that is hard to query is usually a component with an accessibility problem, so these tests improve the markup as well.
Queries
Prefer role and accessible name. The query you reach for is itself an accessibility check.
import { render, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
render(<ProblemCard problem={problem} onOpen={onOpen} />)
// Priority order
screen.getByRole('button', { name: /solve/i }) // best
screen.getByLabelText('Email') // form fields
screen.getByPlaceholderText('Search problems')
screen.getByText(/two sum/i)
screen.getByTestId('problem-card') // last resort
// If getByRole cannot find your button, a screen reader cannot
// either. The failing test is telling you about a real defect.
// get / query / find
getByRole(...) // throws if missing — asserts presence
queryByRole(...) // null if missing — the only way to assert absence
await findByRole(...) // waits for it to appear — async
expect(screen.queryByText('Error')).not.toBeInTheDocument()Interaction and Async
userEvent over fireEvent, and waiting properly.
const user = userEvent.setup()
await user.click(screen.getByRole('button', { name: /run/i }))
await user.type(screen.getByLabelText('Search'), 'two sum')
await user.selectOptions(screen.getByRole('combobox'), 'hard')
await user.keyboard('{Enter}')
// userEvent fires the full sequence a real user produces
// (pointerdown, mousedown, focus, keydown, keypress, input…),
// which catches bugs fireEvent's single synthetic event misses.
// Waiting for a result
expect(await screen.findByText('All tests passed')).toBeInTheDocument()
await waitFor(() => {
expect(mockSubmit).toHaveBeenCalled()
})
// Waiting for something to disappear
await waitForElementToBeRemoved(() => screen.queryByRole('progressbar'))
// Never assert immediately after an async action — the classic
// flaky test is a missing await, not a slow machine.What to Test
Behaviour, not implementation. The test should survive a rewrite of the internals.
// Bad — couples the test to internals
expect(wrapper.state('loading')).toBe(true)
expect(container.querySelector('.spinner')).toBeTruthy()
// Good — describes what the user experiences
it('shows a spinner while running, then the result', async () => {
const user = userEvent.setup()
render(<Solver problem={problem} />)
await user.click(screen.getByRole('button', { name: /run/i }))
expect(screen.getByRole('progressbar')).toBeInTheDocument()
expect(await screen.findByText(/3 of 3 tests passed/i)).toBeInTheDocument()
})
// Worth testing: conditional rendering, gates, error and empty
// states, form validation, callbacks firing with the right payload.
// Not worth testing: that a prop is rendered verbatim, that CSS
// classes exist, or the internals of a library you did not write.
it('locks the editor for a Pro problem when signed out', () => {
render(<Solver problem={{ ...problem, locked: true }} />)
expect(screen.queryByRole('textbox')).not.toBeInTheDocument()
expect(screen.getByRole('link', { name: /upgrade/i })).toBeInTheDocument()
})Key Points to Remember
- 1Query by role and accessible name first; needing getByTestId often signals an accessibility gap
- 2getBy throws, queryBy returns null for absence assertions, findBy waits for an async appearance
- 3userEvent reproduces the full event sequence a real user generates, unlike fireEvent
- 4Missing awaits — not slow machines — cause most flaky component tests
- 5Test conditional rendering, gates, error and empty states; do not test internal state or class names
Interview Questions
Sign in to ask AriaWhy does Testing Library discourage querying by class name or test id?
What is the difference between getByText, queryByText and findByText?
Why prefer userEvent over fireEvent?
Ask Aria about Testing React Components
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.