Unit tests check a single function or component in isolation. Integration tests check that several pieces work correctly together (e.g., a form and its validation logic). End-to-end (E2E) tests (using tools like Playwright or Cypress) drive a real browser through an entire user flow, like signing up and completing a purchase. Most React projects write far more unit/component tests than E2E tests, since E2E tests are slower and more brittle.
import { render, screen } from "@testing-library/react";
import { test, expect } from "vitest";
import Greeting from "./Greeting";
test("renders a greeting with the given name", () => {
render(<Greeting name="Ravi" />);
const heading = screen.getByText("Hello, Ravi");
expect(heading).toBeInTheDocument();
});React Testing Library deliberately encourages testing components the way a real user would interact with them โ finding elements by visible text or accessible role, not by internal implementation details like component names or state variables.
import { render, screen, fireEvent } from "@testing-library/react";
import { test, expect } from "vitest";
import Counter from "./Counter";
test("increments the count when the button is clicked", () => {
render(<Counter />);
const button = screen.getByRole("button", { name: /increase/i });
fireEvent.click(button);
expect(screen.getByText("Count: 1")).toBeInTheDocument();
});This directly tests the counter component from Chapter 12: render it, simulate exactly what a user would do (click the button), then assert the resulting UI reflects the expected new state โ no need to inspect internal useState values directly.
import { render, screen, waitFor } from "@testing-library/react";
import { test, expect, vi } from "vitest";
import Users from "./Users";
test("displays fetched users", async () => {
global.fetch = vi.fn(() =>
Promise.resolve({
json: () => Promise.resolve([{ id: 1, name: "Priya" }])
})
);
render(<Users />);
await waitFor(() => {
expect(screen.getByText("Priya")).toBeInTheDocument();
});
});Testing Chapter 21's data-fetching pattern requires mocking โ replacing the real fetch with a fake version that returns predictable test data, so tests run fast and reliably without depending on a real network connection or external API being available.
Writing a test that checks a component's internal state variable directly (rather than what's actually rendered on screen) creates brittle tests that break whenever you refactor internal code, even if the component's actual behavior for users hasn't changed at all. React Testing Library's getByText/getByRole approach deliberately steers you toward testing what users actually see and do instead.
Write a test for a simple Greeting component that verifies it displays the correct name passed in as a prop.
import { render, screen } from "@testing-library/react";
import { test, expect } from "vitest";
function Greeting({ name }) {
return <h1>Welcome, {name}!</h1>;
}
test("displays the correct name", () => {
render(<Greeting name="Ananya" />);
expect(screen.getByText("Welcome, Ananya!")).toBeInTheDocument();
});
Q Is Vitest the same thing as Jest?
They're similar, competing test runners with largely compatible APIs. Vitest is built specifically to integrate seamlessly with Vite projects (faster startup, native ESM support), while Jest is the older, still widely used standard, especially in Create React App-based projects.
Q Do I need to write tests for every single component?
Not necessarily every one โ prioritize testing components with real logic (validation, calculations, conditional rendering) and critical user flows (checkout, login) over simple, purely presentational components that just display static props.