Testing React
Test what users see and do, not how components work internally. React Testing Library (RTL) encourages this approach.
What to Test
- Renders correctly with different data
- Responds to user interactions (clicks, input)
- Shows the right content based on props or state
- Handles loading, error, and empty states
Don’t test implementation details like internal state or hook calls.
Setup
Install the required packages.
npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom
Configure Vitest with a DOM environment.
// vitest.config.js
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
environment: "jsdom",
setupFiles: ["./setup.js"],
},
});
// setup.js
import "@testing-library/jest-dom";
Basic Test
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Counter from "./Counter";
test("increments count on click", async () => {
render(<Counter />);
const button = screen.getByRole("button", { name: /count/i });
await userEvent.click(button);
expect(button).toHaveTextContent("1");
});
Querying Elements
Use queries that reflect what users see.
screen.getByRole("button", { name: "Submit" }); // Accessible role
screen.getByText("Welcome"); // Text content
screen.getByLabelText("Email"); // Form labels
screen.getByPlaceholderText("Search"); // Placeholder
screen.queryByText("Not here"); // Returns null if missing
Prefer getByRole and getByLabelText over getByTestId.
Testing Async Behavior
test("loads user data", async () => {
render(<UserProfile userId={1} />);
expect(await screen.findByText("John Doe")).toBeInTheDocument();
});
Use findBy for elements that appear after an async operation.
Mocking
Mock API calls with vi.fn().
vi.mock("./api", () => ({
fetchUser: vi.fn(),
}));
import { fetchUser } from "./api";
test("displays error on failure", async () => {
fetchUser.mockRejectedValue(new Error("Network error"));
render(<UserProfile userId={1} />);
expect(await screen.findByText("Error")).toBeInTheDocument();
});
Best Practices
- Test user behavior, not implementation
- Use accessible queries (
getByRole,getByLabelText) - Use
userEventoverfireEventfor realistic interactions - Mock at the network level, not component internals
- Keep tests focused — one behavior per test
Common Mistakes
- Testing internal state or hook return values
- Using
getByTestIdas the first choice - Writing tests that depend on rendering order
- Not cleaning up between tests (RTL handles this automatically)
- Over-mocking — mock only what you need