ReactTestingbeginner
Updated:

React Testing Library Tutorial for Beginners

6 min read

Learn React Testing Library from scratch: setup, your first render test, querying by role, simulating clicks and typing, testing async UI, and best practices.

TL;DR – Quick Answer

React Testing Library is a lightweight tool for testing React components the way a user experiences them — by finding elements on screen and interacting with them, not by inspecting internal state. You render a component, query for text and roles a user would see, simulate clicks and typing with userEvent, and assert on what appears. It pairs with a test runner like Jest or Vitest and is the industry-standard way to test React UIs.

On This Page

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:

  1. getByRole — buttons, headings, textboxes, links. Best choice, and it nudges you toward accessible markup.
  2. getByLabelText — form fields associated with a <label>.
  3. getByText — visible text that is not an interactive element.
  4. 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 await before user.click or user.type. user-event actions are asynchronous, and skipping await causes flaky tests that pass sometimes and fail other times. Always await interactions and findBy queries.

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 screen for queries. It reads cleanly and is the recommended style over destructuring queries from render.

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?
It is used to test React components from the user's point of view. Instead of checking internal state or props, you render the component, look for the text and buttons a user would see, interact with them, and assert on the result. This produces tests that keep passing even when you refactor the internals.
Is React Testing Library the same as Jest?
No. Jest is the test runner and assertion library that finds and executes test files and provides expect. React Testing Library is a smaller library that renders components and queries the DOM. They are used together: Jest runs the test, RTL renders and queries the component.
What is the difference between getBy, queryBy and findBy?
getBy returns an element and throws if it is missing, so use it for things that should be present. queryBy returns null instead of throwing, so use it to assert something is absent. findBy returns a promise that resolves when the element appears, so use it for asynchronous UI that shows up after a fetch.
Why should I query by role instead of by test id?
Querying by role, like getByRole('button'), tests the component the way real users and assistive technology find elements, which also nudges you toward accessible markup. Test ids are an escape hatch that couple tests to implementation details. Reach for getByTestId only when no accessible query works.
How do I test a button click in React Testing Library?
Render the component, get the button with getByRole, then simulate the click with userEvent.click inside an async test. After the click, assert on the DOM change you expect, such as an updated count or a new message. userEvent is preferred over the older fireEvent because it mimics real user interaction more closely.
How do I test components that fetch data?
Mock the network call so the test is fast and deterministic, render the component, then use findBy queries or waitFor to wait for the loaded data to appear. You assert on the final rendered result rather than on the fetch itself. Mocking keeps tests from depending on a live server.

Want to Build Your Career in Java Full Stack with AI?

Join CodeBegun and train with working industry engineers — Explore the Program

Apply for Demo Class →
Siva Prasad Galaba
Founder, CodeBegun · Staff Engineer

Founder of CodeBegun. 15+ years building Java systems at companies like Crunchyroll. Teaches Java, Spring Boot and system design the way the industry actually works, and mentors students through projects, mock interviews and placement preparation.

Technically reviewed by CodeBegun Technical TeamLast reviewed 15 July 2026 LinkedIn
Chat with us