Technology · React

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.

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();
    });

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

Warning: Avoid testing implementation details like component state or internal functions, since refactoring will break tests that depend on the old structure.

React SuspenseReact Testing