Test React components effectively using React Testing Library, Vitest, and common testing patterns.
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();
});// Good: user sees this text
screen.getByText("Welcome");
screen.getByRole("button", { name: /submit/i });
screen.getByLabelText("Email");
// Bad: implementation detail
screen.getByTestId("submit-btn");// vitest.config.js
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: "jsdom"
}
});import { render } from "@testing-library/react";
export function renderWithProviders(ui) {
return render(<ThemeProvider>{ui}</ThemeProvider>);
}it("applies theme colors", () => {
renderWithProviders(<Component />);
// Component now has theme context
});import { vi } from "vitest";
vi.mock("./api", () => ({
fetchUser: vi.fn(() =>
Promise.resolve({ id: 1, name: "Alice" })
)
}));vi.mock("next/router", () => ({
useRouter: vi.fn(() => ({
push: vi.fn(),
pathname: "/"
}))
}));it("displays user data after fetching", async () => {
render(<UserProfile userId="1" />);
// Component fetches user data
const name = await screen.findByText("Alice");
expect(name).toBeInTheDocument();
});// 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();await waitFor(() => {
expect(screen.getByText("Success")).toBeInTheDocument();
});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();
});it("shows success message when isSuccess is true", () => {
render(<Alert isSuccess={true} message="All good!" />);
expect(screen.getByText("All good!")).toBeInTheDocument();
});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();
});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.