Home/Learn/JavaScript & TypeScript/Testing React Components

Testing React Components

Intermediate
Quality

Testing 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.

getByRole first, getByTestId last
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.

userEvent, findBy and waitFor
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.

Assert on what the user sees
// 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 Aria
1

Why does Testing Library discourage querying by class name or test id?

Medium
2

What is the difference between getByText, queryByText and findByText?

Medium
3

Why prefer userEvent over fireEvent?

Medium

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.

Loading discussion…