QuestionsTesting

Testing React components the user's way

Component TestingMediumTesting

How should you query and assert on a React component with Testing Library? Why 'by role' over test IDs or class names?

What it tests

Whether you test components through the accessibility tree the way a user perceives them, not through internals.

Approach & answer

Testing Library's core idea is to test a component the way a USER interacts with it, so your tests give confidence that the real thing works and don't break on refactors. Concretely: render the component, find elements the way a person (or assistive tech) would, interact, and assert on what's visible. That means preferring queries in this priority order: getByRole (with an accessible name, e.g. getByRole('button', {name: /submit/i})) is best because it mirrors how users and screen readers find things and doubles as an accessibility check; then getByLabelText for form fields (you find inputs by their label, as a user does); then getByPlaceholderText, getByText, getByDisplayValue; and only as a last resort getByTestId, an escape hatch for elements with no accessible handle. You explicitly AVOID querying by CSS class or DOM structure (container.querySelector('.btn-primary')) because those are implementation details — rename a class or restructure a div and the test breaks though nothing user-facing changed. Query variants matter too: getBy throws if not found (assert presence), queryBy returns null (assert ABSENCE — the only one for 'should not be there'), findBy returns a promise and retries (for elements that appear after async work). Assert with jest-dom matchers (toBeInTheDocument, toBeVisible, toBeDisabled, toHaveTextContent) which read well and fail with helpful messages. A nice side effect: if you can't query your component by role/label, that's often a real accessibility gap — the test is telling you a screen-reader user would struggle too. The mental model: don't reach into the component; interact with its rendered output the way a human would, and assert on what they'd observe.

Use this technique when

Writing component tests; choosing a query; deciding whether a testid is justified.

Code

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

test('shows a greeting after submitting a name', async () => {
  render(<Greeter />);

  // Query the way a user / screen reader finds things
  await userEvent.type(screen.getByLabelText(/your name/i), 'Ada');
  await userEvent.click(screen.getByRole('button', { name: /greet/i }));

  // Assert on what's visible, not on internal state or classes
  expect(screen.getByText(/hello, ada/i)).toBeInTheDocument();
});

// Avoid: container.querySelector('.greeting') -> couples to markup/CSS

References