Testing5 min read

Testing Library Patterns

Test React components with Testing Library: query priorities, user-event interactions, async testing patterns, and avoiding implementation details.

testing-libraryreactunit-testingcomponents

Testing Library encourages testing components as users interact with them - via visible text, roles, and labels rather than internal IDs or DOM structure. This produces tests that are resilient to refactoring and catch real accessibility issues.

Query priority: `getByRole` > `getByLabelText` > `getByText` > `getByTestId`. Use `user-event` over `fireEvent` - it simulates real user behavior (focus, keyboard events). Test behavior, not implementation. Avoid testing internal state or component methods.

Workflow

  1. Query by role and label (not test IDs)Role-based queries test accessibility as well as functionality - if a screen reader can't find it, neither can the test.
  2. Simulate interactions with user-eventuser-event fires realistic event sequences (pointerover, mousedown, focus, keydown) unlike `fireEvent` which fires single events.
  3. Test async behaviorComponents with async state changes (API calls, debounced input) require async assertions.
  4. Mock APIs and hooksComponent tests should not make real API calls - mock at the right boundary.
  5. Assert on accessible outputAssertions on roles, text, and attributes check both functionality and accessibility.

Output Format

Role-based queries, `userEvent.setup()` for interactions, `findByRole` for async, MSW for API mocking, `renderWithProviders` wrapper, `jest-dom` assertions.

Examples

Form test: `test("submits login form", async () => { const user = userEvent.setup(); render(<LoginForm onSubmit={mockSubmit} />); await user.type(screen.getByLabelText("Email"), "alice@example.com"); await user.type(screen.getByLabelText("Password"), "secret"); await user.click(screen.getByRole("button", { name: "Sign In" })); expect(mockSubmit).toHaveBeenCalledWith({ email: "alice@example.com", password: "secret" }) })`
Async with MSW: `test("shows user name", async () => { server.use(http.get("/api/user", () => HttpResponse.json({ name: "Bob" }))); render(<UserProfile />); expect(await screen.findByText("Bob")).toBeInTheDocument() })`
Error state: `test("shows error on failure", async () => { server.use(http.get("/api/data", () => HttpResponse.json({ error: "Not found" }, { status: 404 }))); render(<DataView />); await waitFor(() => { expect(screen.getByRole("alert")).toHaveTextContent("Not found") }) })`

Edge Cases & Gotchas

  • act() warnings: React state updates in tests must be wrapped in `act()`. Testing Library's async utilities (`findBy*`, `waitFor`) handle this automatically. Avoid manual `act()` calls - they indicate you're testing too close to the implementation
  • Context and providers: use a custom `renderWithProviders` wrapper instead of duplicating provider setup in every test
  • Snapshots: avoid snapshot testing for complex components - they break on any DOM change, creating maintenance burden. Prefer explicit assertions on specific visible text and roles

When to Break These Rules

  • For complex visual output (charts, canvases, custom renders), visual regression testing with Chromatic or Percy is more appropriate than DOM assertions.

Reference Files

  • Testing Library docs: https://testing-library.com/docs/
  • Query priority: https://testing-library.com/docs/queries/about#priority
  • user-event: https://testing-library.com/docs/user-event/intro/
  • MSW: https://mswjs.io/

More skills in this library

Browse all technical skills - Mobile, AI/ML, Web, Backend, Design, and Product.