React Testing Library is a library for testing React components the way a user experiences them. Its guiding principle is that the more your tests resemble the way your software is used, the more confidence they can give you. You assert on visible output and real interactions instead of component internals such as state variables or prop objects.
What the library is and is not
React Testing Library builds on DOM Testing Library and adds helpers for rendering React components and working with the DOM they produce. It is not a test runner, and it does not replace Vitest or Jest. Install it as a development dependency together with its peer dependency.
npm install --save-dev @testing-library/react @testing-library/domThe library is a replacement for Enzyme with the same goal in a harder-to-misuse form. Because it only exposes utilities that work on DOM nodes, it steers you away from inspecting component instances. That guiding principle applies to every query and assertion in this section.
Write the first behavior test
Start with a component that shows a greeting based on a prop.
export default function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}The test renders the component and reads the visible text back. Matchers such as toBeInTheDocument come from the jest-dom companion package, which is configured once in the test setup.
import { render, screen } from "@testing-library/react";
import Greeting from "./Greeting";
test("shows the name in the heading", () => {
render(<Greeting name="Ada" />);
expect(screen.getByText("Hello, Ada!")).toBeInTheDocument();
});The render call mounts the component into a container appended to the document. The screen object exposes queries bound to the whole document, and getByText finds the heading by its visible text. When the assertion passes, a user would see the greeting in the page.
Test what the user does, not how the code works
The library name is about behavior. A counter test should click a button and read the changed text, not reach into a state variable. This component increments its count when clicked.
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Count: {count}
</button>
);
}The test below describes the interaction and its visible result, so it never reads the count variable directly. It clicks the button by its role and name, then reads the label that appears afterward.
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Counter from "./Counter";
test("increases the count when clicked", async () => {
const user = userEvent.setup();
render(<Counter />);
await user.click(screen.getByRole("button", { name: "Count: 0" }));
expect(screen.getByRole("button", { name: "Count: 1" })).toBeInTheDocument();
});If you later split the counter into two components or rename the state variable, the test still passes because the user still sees the same button text. The assertion reads the same label a screen reader would announce.
Why implementation tests break
An implementation test asserts on things users cannot see, such as a received prop object or a helper function call. It passes only while the internals are shaped one way and fails after an innocent refactor.
A test that clicks a button and reads the label survives that refactor because it checks what the user sees. Behavior is the contract that matters, because it is what a real user experiences.
Queries, events, and setup
Queries find elements, and user-event performs interactions. Both deserve their own focus, so the rest of this section covers them in detail.
For choosing the right way to find an element, follow how to query elements in React Testing Library. To simulate clicks, typing, and keyboard input, follow how to test user events in React. When you need the full toolchain wired up, follow how to test React components with Vitest.
Common mistakes
- Testing state variables, props, or function calls instead of visible output.
- Using a test ID when a role or label already matches.
- Asserting the component instance instead of the rendered DOM.
- Making one test cover every concern instead of one behavior.
Use data-testid only as an escape hatch for elements with dynamic text or no accessible match. Most components can be tested entirely through roles, labels, and text.
What to learn next
Start with the queries article to pick the right selector for each element, then move to user events to drive interactions the way a real user would.
Rune AI
Key Insights
- Assert on visible output and real interactions, not internal state or props.
- Install @testing-library/react with @testing-library/dom as a dev dependency.
- Use screen queries and userEvent to drive components the way a user would.
- A test that survives a refactor proves it checks behavior, not internals.
- Use data-testid only when no accessible query matches.
Frequently Asked Questions
Is React Testing Library a test runner?
What does it mean to test implementation details?
Conclusion
React Testing Library keeps tests on the user's side of the screen. Render a component, interact with it through accessible queries and user events, and assert on what appears.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.