After this lesson, you will be able to: Apply React Testing Library philosophy: test behavior, not implementation; mock API calls cleanly.
Tests that survive refactors test what the user sees, not internals.
Query by role + label + text, like a screen reader.
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 initial={0} />);expect(screen.getByText(/count: 0/i)).toBeInTheDocument();await user.click(screen.getByRole("button", { name: /increment/i }));expect(screen.getByText(/count: 1/i)).toBeInTheDocument();});
Mock Service Worker intercepts fetch at the network layer.
// test/handlers.tsimport { http, HttpResponse } from "msw";export const handlers = [http.get("/api/users", () => HttpResponse.json([{ id: "u1", name: "Alex" }])),http.post("/api/users", async ({ request }) => {const body = await request.json();return HttpResponse.json({ id: "u2", ...body }, { status: 201 });}),];// test/setup.tsimport { setupServer } from "msw/node";import { handlers } from "./handlers";export const server = setupServer(...handlers);beforeAll(() => server.listen());afterEach(() => server.resetHandlers());afterAll(() => server.close());
Bad: assert on state shape, hook return values, internal method names. Refactor = test breaks. Good: assert on what the user sees (text, roles, behavior after click). Refactor with same UI = test passes. Rule of thumb: if your test imports useState or jest.spyOn to peek inside, you're testing implementation.
getByTestId everywhere. Couples tests to implementation. Prefer getByRole. Mocking everything, including the component under test. Render the real thing; mock the boundary. Async tests without await. Assertions race the DOM. Always `await findByX` or `await user.click`. Snapshots for everything. They become ignored noise.
Pick the senior answer.
Sign in and purchase access to unlock this lesson.