Module 10 · Testing · Drills
Reading test code is not writing it. Type every one of these yourself, run it, watch it go red then green — that feedback loop is where the skill actually forms.
vitest (or npx playwright test), then click "Show solution" to compare. If yours queries differently but still asserts on user-visible behaviour, that's exactly right. Tick each box as you go; progress is saved in this browser.
Drill 1 render + getByRole
A component renders <h1>DocChat</h1> and a <button>Send</button>. Render it and assert both exist — finding each by its role, not its text.
import { render, screen } from "@testing-library/react" import { Header } from "./Header" it("renders the title and send button", () => { render(<Header />) expect( screen.getByRole("heading", { name: "DocChat" }) ).toBeInTheDocument() expect( screen.getByRole("button", { name: "Send" }) ).toBeInTheDocument() })
Drill 2 userEvent click
A <Counter /> shows "Count: 0" and a button labelled "Increment". Click it once with userEvent and assert it now reads "Count: 1".
import { render, screen } from "@testing-library/react" import userEvent from "@testing-library/user-event" import { Counter } from "./Counter" it("increments on click", async () => { const user = userEvent.setup() render(<Counter />) await user.click(screen.getByRole("button", { name: "Increment" })) expect(screen.getByText("Count: 1")).toBeInTheDocument() })
Always await userEvent and call userEvent.setup() once at the top — it's async because it mimics the full event sequence a real user fires.
Drill 3 getByLabelText
A form has <label>Ask a question</label> tied to a text input. Grab the input by its label, type into it, and assert its value.
import { render, screen } from "@testing-library/react" import userEvent from "@testing-library/user-event" import { AskForm } from "./AskForm" it("accepts typed input", async () => { const user = userEvent.setup() render(<AskForm />) const input = screen.getByLabelText("Ask a question") await user.type(input, "refund policy?") expect(input).toHaveValue("refund policy?") })
Drill 4 async findBy
A <Loader /> shows "Loading…" then, after a delay, "Done". Assert that "Done" eventually appears — using the query that waits.
import { render, screen } from "@testing-library/react" import { Loader } from "./Loader" it("shows Done after loading", async () => { render(<Loader />) // present immediately expect(screen.getByText("Loading…")).toBeInTheDocument() // appears later — findBy polls until it exists expect(await screen.findByText("Done")).toBeInTheDocument() })
Use getBy* for things present now, findBy* for things that appear later, and queryBy* when you need to assert something is absent.
Drill 5 MSW handler
Write an MSW v2 handler for GET /api/documents that returns a JSON array of two documents. Then override it inside one test to return a 500.
import { http, HttpResponse } from "msw" // default handler (in handlers.ts) export const handlers = [ http.get("/api/documents", () => HttpResponse.json([ { id: "1", name: "policy.pdf" }, { id: "2", name: "report.pdf" }, ]) ), ] // per-test override (inside an it block) server.use( http.get("/api/documents", () => new HttpResponse(null, { status: 500 })) )
In v2 it's http.get / http.post (not the old rest.*), and you return an HttpResponse. server.use() overrides for the current test; afterEach(() => server.resetHandlers()) wipes it.
Drill 6 Playwright locator
Write a Playwright test that visits /, fills the "Ask a question" textbox, clicks "Send", and asserts an element containing "refund" becomes visible.
import { test, expect } from "@playwright/test" test("asking shows an answer", async ({ page }) => { await page.goto("/") await page.getByRole("textbox", { name: "Ask a question" }) .fill("What is the refund policy?") await page.getByRole("button", { name: "Send" }).click() await expect(page.getByText(/refund/i)).toBeVisible() })
The await expect(locator).toBeVisible() assertion retries on its own until the element shows up — no manual sleeps. Note Playwright reuses the same accessible getByRole locators you learned in RTL.
<DocumentList /> component through its three states — loading → loaded → empty — driving each with an MSW handler. This is the exact shape of component testing you'll do on the job: one component, every state it can be in, the network mocked.
Build · document list states
The component fetches GET /api/documents on mount. While the request is in flight it shows "Loading documents…". On success it renders a list; if the array is empty it shows "No documents yet." Write three tests.
import { render, screen } from "@testing-library/react" import { http, HttpResponse, delay } from "msw" import { server } from "./test/setup" import { DocumentList } from "./DocumentList" it("shows a loading state first", () => { server.use( http.get("/api/documents", async () => { await delay() // keep the request pending return HttpResponse.json([]) }) ) render(<DocumentList />) expect(screen.getByText("Loading documents…")).toBeInTheDocument() }) it("renders the loaded documents", async () => { server.use( http.get("/api/documents", () => HttpResponse.json([ { id: "1", name: "policy.pdf" }, { id: "2", name: "report.pdf" }, ]) ) ) render(<DocumentList />) // findBy waits for the fetch to resolve and the list to render expect(await screen.findByText("policy.pdf")).toBeInTheDocument() expect(screen.getByText("report.pdf")).toBeInTheDocument() // loading text is gone expect(screen.queryByText("Loading documents…")).not.toBeInTheDocument() }) it("shows an empty state when there are none", async () => { server.use( http.get("/api/documents", () => HttpResponse.json([])) ) render(<DocumentList />) expect(await screen.findByText("No documents yet.")).toBeInTheDocument() })
Three handlers, three states, zero real network. Notice the pattern: getBy* for what's present immediately, findBy* for what arrives after the fetch, queryBy* + not.toBeInTheDocument() to prove the loading text disappeared. Add a fourth test that overrides the handler to return a 500 and asserts an error message — same shape.
Click a card to flip it. Say the answer out loud before you flip — that's the rep that builds storage strength.
getByRole(role, { name }) — mirrors how users and screen readers find things.get throws if absent · query returns null (assert absence) · find is async, retries (appears later).renderHook; read result.current and wrap updates in act().Tick each only if you can do it without looking:
userEvent and await itfindBy* and waitFor