React Testing Library (RTL) encourages testing React components through their observable behavior rather than their internal implementation.
Built on DOM Testing Library, it helps you write readable, maintainable tests that resemble real user interactions.
import { render, screen } from "@testing-library/react";import userEvent from "@testing-library/user-event";import MultiStepForm from "./MultiStepForm";test("completes the multi-step form successfully", async () => {const user = userEvent.setup();const handleSubmit = jest.fn();render(<MultiStepForm onSubmit={handleSubmit} />);// Step 1expect(screen.getByRole("heading", { name: /step 1/i })).toBeInTheDocument();await user.type(screen.getByRole("textbox", { name: /email/i }),"test@mail.com");await user.click(screen.getByRole("button", { name: /next/i }));// Step 2expect(screen.getByRole("heading", { name: /step 2/i })).toBeInTheDocument();await user.type(screen.getByLabelText(/password/i),"123456");await user.click(screen.getByRole("button", { name: /submit/i }));expect(handleSubmit).toHaveBeenCalledWith({email: "test@mail.com",password: "123456",});});
This example assumes the form uses accessible headings and labels and calls onSubmit with the specified object.
Choose queries that reflect how users and assistive technologies identify elements.
| Query | When to use |
|---|---|
getByRole | Buttons, headings, checkboxes, and other accessible elements |
getByLabelText | Form fields associated with labels |
getByPlaceholderText | Inputs when a placeholder is the relevant identifier |
getByText | Visible text that isn’t better queried by role |
getByDisplayValue | Inputs with a specific current value |
Use getByRole where practical because it also encourages accessible markup.
Query variants
getBy...: Expects one matching element immediately.queryBy...: Returns null when no element matches; useful for asserting absence.findBy...: Returns a Promise and waits for an element to appear asynchronously.These matchers are provided by @testing-library/jest-dom.
| Assertion | Use case |
|---|---|
toBeInTheDocument() | Element exists in the DOM |
toHaveTextContent() | Element contains expected text |
toBeDisabled() / toBeEnabled() | Control interaction state |
toHaveValue() | Form field value |
toBeChecked() | Checkbox or radio state |
toHaveAttribute() | HTML or accessibility attributes |
Focus on behavior that matters to users rather than testing every implementation detail.
If your application requires shared providers, wrap RTL’s render function.
// test-utils.tsximport {render as rtlRender,type RenderOptions,} from "@testing-library/react";import type { ReactElement, ReactNode } from "react";import { ThemeProvider } from "../context/theme";function Providers({ children }: { children: ReactNode }) {return <ThemeProvider>{children}</ThemeProvider>;}function render(ui: ReactElement,options?: Omit<RenderOptions, "wrapper">) {return rtlRender(ui, {...options,wrapper: Providers,});}export * from "@testing-library/react";export { render };
Now tests can use your custom render without manually adding ThemeProvider every time.
import { render, screen } from "./test-utils";test("renders the themed component", () => {render(<MyComponent />);expect(screen.getByRole("heading")).toBeInTheDocument();});