What is Testing Library?
Testing Library is a family of libraries that help you test UI components the way a user experiences them. Instead of reaching into a component’s internals, you render it, find elements the way a person would — by their role, label or visible text — and interact with them through realistic events.
The core idea is a reaction against implementation-detail testing. Older approaches rendered a component and then asserted on internal state, props or specific DOM structure. Those tests broke on every refactor while catching very few real bugs. Testing Library flips the perspective: if the user can see it and use it, you can test it, and the test survives changes to how the component works inside.
Rendering and querying
You render a component and then query the resulting DOM through screen.
// LoginForm.test.jsx
import { render, screen } from "@testing-library/react";
import { LoginForm } from "./LoginForm";
test("shows the email field", () => {
render(<LoginForm />);
expect(screen.getByLabelText("Email")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Sign in" })).toBeInTheDocument();
});
screen is the recommended query surface. It searches the whole rendered document, which mirrors how a user sees the page rather than a particular container.
Choosing the right query
Testing Library provides many queries, and the order matters. Prefer the ones closest to the user’s perspective:
- getByRole — buttons, links, headings, inputs and more, matched by accessible name. This is the best default.
- getByLabelText — form controls found by their label.
- getByPlaceholderText — when a placeholder is the only available label.
- getByText — non-interactive text content.
- getByDisplayValue — the current value of a form element.
- getByAltText — images by their alt text.
- getByTitle and getByTestId — last resorts.
// queries.jsx
screen.getByRole("heading", { level: 1, name: "Dashboard" });
screen.getByLabelText("Password");
screen.getByText("Welcome back");
screen.getByAltText("Company logo");
screen.getByTestId("chart");
getByRole is powerful because it doubles as an accessibility check. If a control has no accessible name, the query fails — which means your markup is likely inaccessible too.
Each query has variants: getBy throws if nothing matches, queryBy returns null, and findBy returns a promise that waits. Use queryBy to assert that something is absent, and findBy after an async update.
Interacting with userEvent
userEvent simulates the full sequence of events a real user generates.
// Search.test.jsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Search } from "./Search";
test("submits the query", async () => {
const onSearch = vi.fn();
render(<Search onSearch={onSearch} />);
await userEvent.type(screen.getByLabelText("Search"), "vitest");
await userEvent.click(screen.getByRole("button", { name: "Go" }));
expect(onSearch).toHaveBeenCalledWith("vitest");
});
userEvent handles focus, keyboard and pointer events together, which catches bugs that a single fireEvent.change misses. Prefer it for everything except the rare case it does not cover.
Async behaviour
Most components update asynchronously, whether from a fetch, a timer or a transition. Use findBy and waitFor instead of arbitrary delays.
// Users.test.jsx
test("renders users after loading", async () => {
render(<Users />);
expect(screen.getByText("Loading…")).toBeInTheDocument();
const items = await screen.findAllByRole("listitem");
expect(items).toHaveLength(3);
});
findBy waits for a matching element and fails with a helpful message if it never appears. waitFor is for waiting on an assertion or a condition that is not a simple element lookup. Never use real timeouts; they make tests slow and flaky.
Assertions with jest-dom
The companion @testing-library/jest-dom package adds matchers that read naturally for DOM state.
// matchers.test.jsx
expect(button).toBeDisabled();
expect(input).toHaveValue("[email protected]");
expect(dialog).not.toBeInTheDocument();
expect(heading).toHaveTextContent("Dashboard");
expect(link).toHaveAttribute("href", "/docs");
These matchers make intent clearer than raw DOM property checks and produce better failure messages.
Accessibility-first testing
Because getByRole relies on accessible roles and names, writing tests this way surfaces accessibility problems early. If you cannot find a button by role, screen reader users cannot find it either. Adding getByRole and toHaveAccessibleName to your tests is one of the cheapest ways to improve accessibility, and it often replaces the need for a separate automated audit.
Testing Library across frameworks
The library has adapters for React, Vue, Svelte, Angular and others. The query API is shared, so the mental model transfers even if the setup differs. The React adapter is the most widely used and the one shown here.
Best practices
- Query by role first, then label, then text; use test ids only as a last resort.
- Use
userEventinstead offireEvent. - Await
findByandwaitForrather than using delays. - Assert on what the user sees, not on internal state.
- Keep tests focused on one behaviour each.
- Clean up automatically between tests; Testing Library does this by default.
- Treat a failed
getByRoleas a hint about an accessibility problem.
Common mistakes
- Reaching for
container.querySelectorand testing class names. - Using
fireEventwhenuserEventwould catch more. - Arbitrary
setTimeoutwaits instead offindBy. - Snapshotting the whole rendered tree.
- Asserting on component state or props instead of the DOM.
- Adding test ids to everything and losing the accessibility benefit.
Where to go next
Testing Library is the component layer of a solid test strategy. Run it with Vitest or Jest, deepen your React knowledge so components are testable, and cover complete journeys with Playwright. Then take a component you already built and write a test from the user’s perspective.