Testing — Jest/Vitest

Course 3 · Ch 4
Testing — Jest/Vitest and React Testing Library
Confirming components behave correctly without manually clicking through the app to check, every time

Every project so far has been verified by hand — running the app and clicking around to confirm something works. That's fine for small projects, but doesn't scale, and gives nothing to catch a regression introduced weeks later by an unrelated change. React Testing Library (paired with a test runner — Jest, or Vite's own Vitest) lets that same kind of verification be written once and run automatically.

Jest vs Vitest
Jest is the long-standing standard test runner for JavaScript/React projects (used by Next.js and older Create React App setups); Vitest is Vite's own equivalent, built to be a near drop-in replacement with an almost identical API (describe, test, expect all work the same way). Since every project in this course has used Vite, Vitest is the natural fit going forward — everything in this chapter applies equally to either, since React Testing Library itself doesn't care which one is running it.

The Core Philosophy

React Testing Library is built around one guiding rule: test what a user actually sees and does, not a component's internal implementation. That means querying for visible text, labels, and accessible roles — never reaching into a component's state, props, or internal function calls directly. A test written this way keeps passing through a refactor that changes how a component works internally, as long as it still behaves the same way from the outside — exactly the kind of test that's actually worth having.

A First Test

// Greeting.jsx function Greeting({ name }) { return <h1>Hello, {name}!</h1>; } // Greeting.test.jsx import { render, screen } from "@testing-library/react"; import { describe, test, expect } from "vitest"; import Greeting from "./Greeting"; describe("Greeting", () => { test("renders the given name", () => { render(<Greeting name="Philip" />); expect(screen.getByText("Hello, Philip!")).toBeInTheDocument(); }); });

render(<Greeting name="Philip" />) renders the component into a virtual DOM the test can inspect; screen.getByText(...) searches that rendered output for matching text, throwing immediately if nothing matches; expect(...).toBeInTheDocument() is the actual assertion — confirming the element was actually found.

Simulating User Interaction

// Counter.jsx — the same component from Fundamentals Chapter 3 // Counter.test.jsx import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import Counter from "./Counter"; test("increments the count when +1 is clicked", async () => { const user = userEvent.setup(); render(<Counter />); await user.click(screen.getByRole("button", { name: "+1" })); expect(screen.getByText("Count: 1")).toBeInTheDocument(); });

userEvent simulates real user interaction more faithfully than directly firing a raw DOM event — clicking, typing, and focusing all go through the same library, recommended over the lower-level fireEvent for exactly that reason. getByRole("button", { name: "+1" }) finds the button by its accessible role and visible label — the same way a screen reader or a person scanning the page would identify it, rather than relying on a CSS class or test-specific attribute.

Testing a Form

test("calls onSubmit with the typed value", async () => { const user = userEvent.setup(); const handleSubmit = vi.fn(); // jest.fn() in Jest render(<SearchForm onSubmit={handleSubmit} />); await user.type(screen.getByRole("textbox"), "react"); await user.click(screen.getByRole("button", { name: "Search" })); expect(handleSubmit).toHaveBeenCalledWith("react"); });

vi.fn() (or jest.fn()) creates a mock function — a fake function the test can pass in as a prop and later inspect, asking "was this called, and with what?" without needing a real implementation behind it. Passing a mock as onSubmit lets the test confirm SearchForm calls it correctly with the typed value, without needing the component to actually be connected to a real API anywhere.

Don't test implementation details
Reaching for a component's internal state, calling its functions directly, or asserting on a CSS class name couples a test to how a component is built rather than what it does — exactly the kind of test that breaks on a harmless refactor (renaming a variable, switching from useState to useReducer) even though the component's actual behavior never changed. Querying by role, label, and visible text — the same things a real user would see and interact with — keeps a test meaningful regardless of what changes underneath.

QueryPrefer it when...
getByRoleAlmost always the first choice — matches how assistive tech sees the page
getByLabelTextForm fields specifically
getByTextPlain visible text with no clearer role/label
getByTestIdLast resort — nothing else can uniquely identify the element

Coding Challenges

Challenge 1

Write a test for a Greeting component (accepting a name prop) confirming it renders the expected text for at least two different names.

📄 View solution
Challenge 2

Write a test for a Counter component (from Fundamentals Chapter 3) using userEvent to click the +1 button twice, asserting the displayed count updates correctly each time.

📄 View solution
Challenge 3

Write a test for a controlled search input + submit button, using userEvent.type to type a value and a mock function (vi.fn()/jest.fn()) passed as an onSubmit prop, asserting it was called with the typed value.

📄 View solution

Chapter 4 Quick Reference

  • render() mounts a component for testing; screen.getByX queries its rendered output
  • userEvent simulates real clicks/typing — preferred over the lower-level fireEvent
  • vi.fn() / jest.fn() — a mock function, for asserting it was called (and with what)
  • Query priority: getByRole > getByLabelText > getByText > getByTestId (last resort)
  • Test what a user sees/does — never a component's internal state or implementation details
  • Vitest (Vite's test runner) and Jest share nearly identical syntax — everything here applies to both
  • Next chapter: advanced patterns — compound components, render props, higher-order components