Testing Library Patterns
Test React components with Testing Library: query priorities, user-event interactions, async testing patterns, and avoiding implementation details.
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
- 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.
- Simulate interactions with user-eventuser-event fires realistic event sequences (pointerover, mousedown, focus, keydown) unlike `fireEvent` which fires single events.
- Test async behaviorComponents with async state changes (API calls, debounced input) require async assertions.
- Mock APIs and hooksComponent tests should not make real API calls - mock at the right boundary.
- 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#priorityuser-event: https://testing-library.com/docs/user-event/intro/MSW: https://mswjs.io/