Tests give you the confidence to change code without breaking it. In React, the tool that does this well is React Testing Library (RTL), and its whole philosophy fits in one line: test your components the way a user actually uses them. Users do not read your state — they read text on screen and click buttons. So that is what your tests do too.
This tutorial takes you from an empty project to writing real tests: rendering a component, finding elements by role and text, simulating clicks and typing, testing asynchronous data, and following the habits that keep tests from becoming a maintenance burden.
Why test from the user's point of view
Older testing styles reached into a component to check its internal state or called its methods directly. The problem: rename a state variable and the test breaks, even though the UI works perfectly. Those tests are tied to how the component is built, not what it does.
RTL flips this. It renders your component into a real DOM (via jsdom) and gives you queries that find elements the way a person would — "the button labelled Submit", "the text that says Welcome". Refactor the internals however you like; as long as the user-visible behaviour is the same, the test still passes. That resilience is why RTL is the standard.
Pro tip: The guiding rule from RTL's author is: "The more your tests resemble the way your software is used, the more confidence they give you." Whenever you are unsure how to query something, ask how a real user would find it on the page.
Setup
If you created your app with Vite — see the Vite setup guide — pair RTL with Vitest; a Create React App project ships with Jest. The install is the same handful of packages:
npm install --save-dev @testing-library/react @testing-library/jest-dom \
@testing-library/user-event vitest jsdom
@testing-library/react renders components, jest-dom adds readable matchers like
toBeInTheDocument(), and user-event simulates real interaction. Set your test
environment to jsdom so a fake browser DOM exists, then you are ready to write tests in
files named Something.test.jsx.
Your first test: does it render?
Start with the simplest possible assertion — the component renders the text you expect. Take a small greeting component:
// Greeting.jsx
export function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
// Greeting.test.jsx
import { render, screen } from "@testing-library/react";
import { Greeting } from "./Greeting";
test("shows a personalised greeting", () => {
render(<Greeting name="Anil" />);
const heading = screen.getByText("Hello, Anil!");
expect(heading).toBeInTheDocument();
});
Three moves make up almost every test: render puts the component in the DOM, screen
lets you query it, and expect(...).toBeInTheDocument() asserts what you found is really
there. Everything else is variations on this rhythm.
Choosing the right query
RTL gives you a family of queries, and picking the right one is most of the skill. Two axes matter: what you match on, and when the element appears.
Prefer queries in this order — they mirror how users and assistive technology find things:
getByRole— buttons, headings, textboxes, links. Best choice, and it nudges you toward accessible markup.getByLabelText— form fields associated with a<label>.getByText— visible text that is not an interactive element.getByTestId— last resort, when nothing above works.
The prefix tells you the timing behaviour:
| Prefix | Missing element | Use for |
|---|---|---|
getBy |
throws immediately | something that should be present now |
queryBy |
returns null |
asserting something is absent |
findBy |
waits, returns a promise | something that appears after async work |
screen.getByRole("button", { name: /submit/i }); // present now
expect(screen.queryByText("Error")).not.toBeInTheDocument(); // absent
await screen.findByText("Loaded!"); // appears later
Testing user interactions
Rendering is only half the story. Real components respond to clicks and typing, and
user-event simulates those the way a browser would. Here is a counter and its test:
// Counter.jsx
import { useState } from "react";
export function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
}
// Counter.test.jsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Counter } from "./Counter";
test("increments when clicked", async () => {
const user = userEvent.setup();
render(<Counter />);
const button = screen.getByRole("button", { name: /count: 0/i });
await user.click(button);
expect(screen.getByRole("button", { name: /count: 1/i })).toBeInTheDocument();
});
Notice the test never touches useState or reads count directly. It clicks the button
and checks the label changed — exactly what a user would notice. Typing works the same way
with user.type(input, "hello"), which is how you test the
controlled form inputs you build
with useState, the hook covered in the
useState and useEffect guide.
Common mistake: Forgetting
awaitbeforeuser.clickoruser.type.user-eventactions are asynchronous, and skippingawaitcauses flaky tests that pass sometimes and fail other times. Alwaysawaitinteractions andfindByqueries.
Testing components that fetch data
Most real components load data. You do not want tests hitting a live server — that is slow and unreliable — so you mock the network and wait for the result to appear.
test("renders users after loading", async () => {
globalThis.fetch = vi.fn(() =>
Promise.resolve({ json: () => Promise.resolve([{ id: 1, name: "Divya" }]) })
);
render(<UserList />);
// While loading, a spinner or message shows
expect(screen.getByText(/loading/i)).toBeInTheDocument();
// findBy waits for the fetched data to render
expect(await screen.findByText("Divya")).toBeInTheDocument();
});
The pattern: mock fetch, render, assert the loading state, then await a findBy query
for the loaded content. You test the outcome the user sees, not the fetch call itself.
The useEffect that triggers this fetch is explained in the
hooks introduction.
Grouping tests and the arrange-act-assert rhythm
As your test files grow, structure keeps them readable. Group related tests for one
component inside a describe block, and give each test a name that reads like a sentence
describing behaviour. Inside every test, follow the arrange-act-assert rhythm: set up the
component, perform an action, then check the result.
import { describe, test, expect } from "vitest";
describe("Counter", () => {
test("starts at zero", () => {
render(<Counter />); // arrange
expect(screen.getByRole("button", { name: /count: 0/i })).toBeInTheDocument();
});
test("increments on click", async () => {
const user = userEvent.setup();
render(<Counter />); // arrange
await user.click(screen.getByRole("button")); // act
expect(screen.getByText(/count: 1/i)).toBeInTheDocument(); // assert
});
});
A well-named test is documentation: when it fails in CI six months from now, the name alone tells the next developer what broke. That readability is a large part of why teams value a test suite at all — it captures intent, not just correctness.
Habits that keep tests healthy
A few practices separate a test suite people trust from one they ignore:
- Query by role and text, not by class or id. Your tests survive refactors and confirm accessibility at the same time.
- Test behaviour, not implementation. Assert on what renders, never on internal state variables or which functions were called internally.
- One clear intention per test. A test named "increments when clicked" should test exactly that, so a failure points straight at the cause.
- Use
screenfor queries. It reads cleanly and is the recommended style over destructuring queries fromrender.
Where testing takes you next
Testing is a skill hiring managers look for because it signals you write code others can
maintain. Being able to explain why RTL queries by role and what the difference between
getBy and findBy is will stand out in the
React freshers interview. Start small: add one render
test to a component you already built, then a click test, then an async test.
At CodeBegun's Java Full Stack with AI program in Hyderabad, students write RTL tests for the components in their capstone project, so testing becomes a normal part of building rather than an afterthought. Return to the React learning hub for the components worth testing next, and remember the one rule that carries the whole library: test your app the way your users use it.
Frequently Asked Questions
What is React Testing Library used for?
Is React Testing Library the same as Jest?
What is the difference between getBy, queryBy and findBy?
Why should I query by role instead of by test id?
How do I test a button click in React Testing Library?
How do I test components that fetch data?
Want to Build Your Career in Java Full Stack with AI?
Join CodeBegun and train with working industry engineers — Explore the Program

