ESC

Type to search the knowledge base.

Testing UI with Testing Library

Test user-facing behavior — queries by role and label, async utilities, and what not to assert.

intermediate1 min read
  • testing
  • react
  • a11y

The guiding principle from Testing Library: test software the way users use it. Prefer roles, labels, and text over CSS selectors and internal state.

Query priority

  1. getByRole — buttons, links, headings, dialogs
  2. getByLabelText / getByPlaceholderText — form fields
  3. getByText — non-interactive content
  4. getByTestId — last resort
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { LoginForm } from './LoginForm';

test('submits email and password', async () => {
  const user = userEvent.setup();
  const onSubmit = vi.fn();
  render(<LoginForm onSubmit={onSubmit} />);

  await user.type(screen.getByLabelText(/email/i), 'a@b.com');
  await user.type(screen.getByLabelText(/password/i), 'secret');
  await user.click(screen.getByRole('button', { name: /sign in/i }));

  expect(onSubmit).toHaveBeenCalledWith({
    email: 'a@b.com',
    password: 'secret',
  });
});

If getByRole cannot find a control, your UI may have an accessibility bug, not just a test problem.

Async

expect(await screen.findByRole('alert')).toHaveTextContent(/saved/i);

Use findBy* for content that appears after fetch. Prefer waitFor sparingly when asserting non-element conditions.

What not to do

  • Snapshot entire pages as the only test
  • Assert on class names that are design-system internals
  • Test implementation details (useState values)
  • Mock so much that the test only verifies the mock

Coverage of failure paths

Good suites include empty, loading, error, and unauthorized states — the same states your design system should define.

Related guides