React Testing Best Practices

Test React components effectively using React Testing Library, Vitest, and common testing patterns.

TL;DR

  1. 01Test user behavior, not implementation details.
  2. 02Use React Testing Library to query elements as users see them.
  3. 03Mock external dependencies but test component logic thoroughly.

Tips

  1. 01Test user behavior and critical paths thoroughly, even if it means longer tests — catching real bugs is more important than test speed.

Warnings

  1. 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