Testing React components ensures your UI works correctly and doesn't break with changes. The standard approach uses Jest as the test runner and React Testing Library for component rendering.
1. Unit testing with React Testing Library:
1import { render, screen, fireEvent } from "@testing-library/react";2import Counter from "./Counter";34describe("Counter", () => {5 it("renders initial count", () => {6 render(<Counter />);7 expect(screen.getByText("Count: 0")).toBeInTheDocument();8 });910 it("increments count on button click", () => {11 render(<Counter />);12 const button = screen.getByRole("button", { name: /increment/i });13 fireEvent.click(button);14 expect(screen.getByText("Count: 1")).toBeInTheDocument();15 });1617 it("respects initial count prop", () => {18 render(<Counter initialCount={5} />);19 expect(screen.getByText("Count: 5")).toBeInTheDocument();20 });21});
2. Testing async operations:
1import { render, screen, waitFor } from "@testing-library/react";2import UserProfile from "./UserProfile";3import { fetchUser } from "./api";45jest.mock("./api");67it("loads and displays user data", async () => {8 (fetchUser as jest.Mock).mockResolvedValue({9 name: "Alice",10 email: "alice@example.com",11 });1213 render(<UserProfile userId="123" />);1415 // Wait for async data to load16 expect(await screen.findByText("Alice")).toBeInTheDocument();17 expect(screen.getByText("alice@example.com")).toBeInTheDocument();18 expect(fetchUser).toHaveBeenCalledWith("123");19});2021it("shows error state", async () => {22 (fetchUser as jest.Mock).mockRejectedValue(new Error("Not found"));2324 render(<UserProfile userId="999" />);2526 expect(await screen.findByText("Error loading user")).toBeInTheDocument();27});
3. Testing hooks:
1import { renderHook, act } from "@testing-library/react";2import useCounter from "./useCounter";34describe("useCounter", () => {5 it("increments counter", () => {6 const { result } = renderHook(() => useCounter(0));78 act(() => result.current.increment());9 expect(result.current.count).toBe(1);1011 act(() => result.current.increment());12 expect(result.current.count).toBe(2);13 });1415 it("resets counter", () => {16 const { result } = renderHook(() => useCounter(5));1718 act(() => result.current.reset());19 expect(result.current.count).toBe(0);20 });21});
4. Integration testing:
1import { render, screen } from "@testing-library/react";2import userEvent from "@testing-library/user-event";3import Checkout from "./Checkout";45it("completes checkout flow", async () => {6 const user = userEvent.setup();7 render(<Checkout />);89 await user.click(screen.getByText("Add to Cart"));10 await user.click(screen.getByText("Proceed to Checkout"));11 await user.type(screen.getByLabelText(/card number/i), "4242424242424242");12 await user.type(screen.getByLabelText(/expiry/i), "12/25");13 await user.click(screen.getByText("Pay Now"));1415 expect(await screen.findByText("Order confirmed")).toBeInTheDocument();16});
Best practices:
screen.getByRole over getByTestId — tests accessible UIuserEvent over fireEvent for more realistic user interactions