Testing

Render, Query, Interact — That's the Whole Library

Testing

React Testing Library tests components from the user's perspective. Three things — render the component, query the DOM, interact like a user.

5 min read Level 2/5 #react#testing#react-testing-library
What you'll learn
  • Set up Vitest + React Testing Library
  • Render a component and query it
  • Simulate user interaction

The dominant testing approach in React is React Testing Library (RTL) — write tests from the user’s perspective, not from implementation details.

Install

For a Vite project, use Vitest as the test runner:

npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom

In vite.config.js:

export default {
  test: { environment: "jsdom", globals: true, setupFiles: "./vitest.setup.js" },
};

In vitest.setup.js:

import "@testing-library/jest-dom";

The Three Steps

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

test("increments on click", async () => {
  // 1. render
  render(<Counter />);

  // 2. query
  const button = screen.getByRole("button", { name: /count/i });
  expect(button).toHaveTextContent("0");

  // 3. interact
  await userEvent.click(button);
  expect(button).toHaveTextContent("1");
});

Querying

Always prefer role-based queries — they’re how assistive tech finds elements:

QueryFinds
getByRole("button", { name })<button> by its visible text/label
getByRole("textbox", { name })<input> by its label
getByLabelText("Email")An input by its <label>
getByText("Sign up")Any element with that text
getByTestId("save")Last resort — uses data-testid

getBy* throws if not found, queryBy* returns null, findBy* waits async.

Interactions With user-event

const user = userEvent.setup();

await user.click(button);
await user.type(input, "hello");
await user.tab();
await user.keyboard("{Enter}");

userEvent simulates real user actions (typing fires multiple events, etc.) better than the older fireEvent.

Async Updates

For things that happen after a fetch or timer, use findBy*:

test("loads users", async () => {
  render(<UserList />);
  expect(await screen.findByText("Ada Lovelace")).toBeInTheDocument();
});

findBy* retries for ~1 second by default — great fit for async UIs.

What NOT To Test

  • Implementation details (props, state, hook internals)
  • Whether useState was called (it doesn’t matter)
  • Specific class names — query by what the user sees

If your test breaks when you refactor the internals but the user experience is unchanged, the test is too tightly coupled.

What TO Test

  • “Clicking the button increments the counter”
  • “Submitting the form sends the right data”
  • “If the API errors, the user sees an error message”

Tests that read like a description of the feature → tests that survive refactors.

Up Next

Accessibility — make sure everyone can use what you built.

Accessibility →