Testing React Components

Course 1 · Ch 4
Testing React Components
Putting Chapter 3's philosophy into practice — rendering, firing real interactions, and the truth about snapshot testing

Chapter 3 established the philosophy and the query system. This chapter puts both into practice — rendering real components, simulating genuine user interaction, and asserting on what actually changes on screen.

Rendering and Asserting on Output

function Greeting({ name }) { return <h1>Hello, {name}!</h1>; } it("greets the given name", () => { render(<Greeting name="Ada" />); expect(screen.getByText("Hello, Ada!")).toBeInTheDocument(); });

Firing Events: fireEvent vs. userEvent

fireEvent dispatches a single, low-level DOM event directly. userEvent (from @testing-library/user-event) simulates the full, realistic sequence of events a real browser fires for that interaction — typing one character involves keydown, keypress, input, and keyup in order; a click involves pointer and focus events too. Testing Library's own current recommendation: prefer userEvent whenever possible, since it more closely resembles real usage — directly extending Chapter 3's guiding principle.

// Preferred — realistic event sequence await userEvent.click(screen.getByRole("button", { name: "Submit" })); await userEvent.type(screen.getByLabelText("Email"), "ada@example.com"); // Lower-level — dispatches exactly one raw event fireEvent.change(screen.getByLabelText("Email"), { target: { value: "ada@example.com" } });

fireEvent vs. userEvent

fireEvent

Dispatches exactly one raw DOM event. Fast, but can miss real browser behavior (e.g. skipping focus changes).

userEvent (preferred)

Simulates the full, realistic event sequence a real user's action would trigger — closer to actual usage.

Asserting on Conditional Rendering

Combining Chapter 3's query trio with real interaction — confirming something is absent, triggering a change, then confirming it appears:

it("shows an error only after an invalid submission", async () => { render(<SignupForm />); expect(screen.queryByText("Email is required")).not.toBeInTheDocument(); await userEvent.click(screen.getByRole("button", { name: "Sign Up" })); expect(await screen.findByText("Email is required")).toBeInTheDocument(); });

Snapshot Testing: What It Is

expect(container).toMatchSnapshot() captures the entire rendered output into a stored file — future runs diff against it and flag any difference. The appeal is real: fast to write, catches unintended changes. The real problem is just as real: a snapshot captures everything, including implementation details Chapter 1 and 3 have been warning about all along. A tiny, intentional style tweak can produce a giant diff that gets rubber-stamped without real review — a well-known failure mode called "snapshot blindness."

When Snapshots Are (and Aren't) a Good Fit

Targeted Assertions vs. Full Snapshots

Targeted getBy/queryBy Assertions

Specific, meaningful, and resistant to unrelated changes — the right default for behavior.

Full Snapshots

Broad, brittle, and prone to blind "update and move on" fixes — reserve for small, stable, presentational components only.

fireEvent

One raw DOM event — useful for low-level cases userEvent doesn't cover.

userEvent (preferred)

A realistic multi-event sequence matching actual browser behavior — the default choice.

Conditional Rendering Pattern

queryBy to confirm absence → interact → getBy/findBy to confirm appearance.

Snapshot Pitfall

Captures everything, including irrelevant details — easy to blindly "update" without real review.

userEvent Methods Are Async
Modern userEvent (v14+) methods return Promises, mirroring real browser event timing — always await userEvent.click(...) / await userEvent.type(...). Forgetting await doesn't throw an error; it just silently lets the test move on before the interaction has actually finished, producing confusing, intermittent failures.
Blindly Pressing "Update Snapshot" Defeats the Whole Point
Running --updateSnapshot (or pressing u in watch mode) without actually reading the diff turns a snapshot test into a rubber stamp — it approves whatever the new output happens to be, intentional bug or not. A snapshot test only provides value if a real human actually looks at what changed before accepting it as correct.

Coding Challenges

Challenge 1

Write a test for a Counter component that starts at 0 and increments when a button labeled "Increment" is clicked, using userEvent and getByRole/getByText, confirming the displayed count changes from "0" to "1".

📄 View solution
Challenge 2

Write a test for a PasswordField component that shows a "Passwords do not match" message only when a confirm-password input differs from the password input, using queryByText to confirm the message's absence and presence at the right moments.

📄 View solution
Challenge 3

Explain why a snapshot test of a large, frequently-edited form component is more likely to cause "snapshot blindness" than a snapshot test of a small, rarely-changed Icon component — and propose a more targeted alternative test for the form component.

📄 View solution

Chapter 4 Quick Reference

  • fireEvent — dispatches one raw DOM event; userEvent (preferred) — simulates a realistic multi-event sequence
  • await every userEvent call — its methods are async, mirroring real browser timing
  • Conditional rendering pattern: queryBy (confirm absence) → interact → getBy/findBy (confirm appearance)
  • toMatchSnapshot() — captures full rendered output; diffs on every future run
  • Snapshots capture implementation details too — small/stable/presentational components are the right fit, not large or frequently-changing ones
  • Blindly accepting a snapshot update without reading the diff defeats the test's entire purpose
  • Next chapter: Testing Hooks & State — renderHook, useState/useEffect, controlled forms, and waiting for async updates