React Testing Best Practices
Test React components effectively using React Testing Library, Vitest, and common testing patterns.
TL;DR
- 01Test user behavior, not implementation details.
- 02Use React Testing Library to query elements as users see them.
- 03Mock external dependencies but test component logic thoroughly.
Tips
- 01Test user behavior and critical paths thoroughly, even if it means longer tests — catching real bugs is more important than test speed.
Warnings
- 01Avoid testing implementation details like component state or internal functions, since refactoring will break tests that depend on the old structure.
Testing User Behavior
- Test what users see and do, not how components are built.
import { render, screen } from "@testing-library/react"; import Button from "./Button"; it("shows clicked message when button is clicked", async () => { render(<Button />); const button = screen.getByRole("button", { name: /click me/i }); await userEvent.click(button); expect(screen.getByText("You clicked!")).toBeInTheDocument(); }); - Query elements like users would: by text, role, label, not by className.
// Good: user sees this text screen.getByText("Welcome"); screen.getByRole("button", { name: /submit/i }); screen.getByLabelText("Email"); // Bad: implementation detail screen.getByTestId("submit-btn"); - Avoid testing component state or internal functions.
- Focus on inputs, outputs, and user interactions.
Setup and Utilities
- Configure Vitest with React Testing Library setup.
// vitest.config.js import { defineConfig } from "vitest/config"; import react from "@vitejs/plugin-react"; export default defineConfig({ plugins: [react()], test: { globals: true, environment: "jsdom" } }); - Create a custom render function to set up providers.
import { render } from "@testing-library/react"; export function renderWithProviders(ui) { return render(<ThemeProvider>{ui}</ThemeProvider>); } - Use this custom render in all tests to reduce boilerplate.
it("applies theme colors", () => { renderWithProviders(<Component />); // Component now has theme context });
Mocking Dependencies
- Mock external API calls to keep tests fast and isolated.
import { vi } from "vitest"; vi.mock("./api", () => ({ fetchUser: vi.fn(() => Promise.resolve({ id: 1, name: "Alice" }) ) })); - Mock Next.js router for navigation testing.
vi.mock("next/router", () => ({ useRouter: vi.fn(() => ({ push: vi.fn(), pathname: "/" })) })); - Avoid mocking everything — only mock slow or external things.
- Test component logic thoroughly with real implementations.
Async Testing
- Use async/await for testing async code like API calls.
it("displays user data after fetching", async () => { render(<UserProfile userId="1" />); // Component fetches user data const name = await screen.findByText("Alice"); expect(name).toBeInTheDocument(); }); - Use findBy for elements that appear after async operations.
// findBy waits for the element to appear const element = await screen.findByText("Loaded"); // getBy fails immediately if element doesn't exist expect(() => screen.getByText("Loaded")).toThrow(); - Use waitFor for complex async scenarios.
await waitFor(() => { expect(screen.getByText("Success")).toBeInTheDocument(); });
Common Testing Patterns
- Test form submission with userEvent.
it("submits form with email", async () => { const user = userEvent.setup(); render(<LoginForm />); await user.type(screen.getByLabelText("Email"), "test@example.com"); await user.type(screen.getByLabelText("Password"), "password"); await user.click(screen.getByRole("button", { name: /login/i })); expect(screen.getByText("Welcome")).toBeInTheDocument(); }); - Test conditional rendering based on props.
it("shows success message when isSuccess is true", () => { render(<Alert isSuccess={true} message="All good!" />); expect(screen.getByText("All good!")).toBeInTheDocument(); }); - Test error handling and edge cases.
it("shows error when API fails", async () => { vi.mocked(fetchUser).mockRejectedValue(new Error("API failed")); render(<UserProfile userId="1" />); expect(await screen.findByText("Error loading user")).toBeInTheDocument(); });
FAQ
React Testing Library is the current standard and is officially recommended by the React team. Enzyme is largely unmaintained and encourages querying internal state and component structure, which makes tests fragile when you refactor.
Use findBy* queries (e.g., await screen.findByText('Loaded')) or wrap assertions in await waitFor(() => expect(...)) after triggering the action. For realistic network mocking across many tests, consider Mock Service Worker (msw) instead of per-test fetch stubs.
userEvent simulates the full sequence of browser events a real user triggers (e.g., pointerover, focus, keydown, input, keyup on a type), while fireEvent dispatches a single synthetic event. Use userEvent from @testing-library/user-event for interactions like typing and clicking to catch bugs that depend on event ordering.
Call jest.mock('axios') at the top of your test file, then configure return values with axios.get.mockResolvedValue({ data: yourData }) per test. Reset mocks between tests using jest.clearAllMocks() in a beforeEach to prevent state from leaking across test cases.
Use getBy* when the element must exist synchronously (throws if missing), queryBy* when asserting an element is absent (returns null instead of throwing), and findBy* for elements that appear asynchronously (returns a promise). Prefer queries in this priority order: ByRole, ByLabelText, ByPlaceholderText, ByText — they mirror how users and assistive technology discover elements.