React Testing Library Reference: Queries, userEvent, Async, MSW & Accessibility
React Testing Library tests components the way users interact with them — through the DOM, not through component internals. The guiding principle: test what the user sees and does, not how your component is structured. This means no testing state directly, no testing which functions were called inside a component — only what ends up rendered and what happens when users interact with it. The biggest gotcha: async state updates require await waitFor() or await findBy*().
1. Queries — Finding Elements
getBy*, queryBy*, findBy*, priority order, and when to use each
// npm install -D @testing-library/react @testing-library/jest-dom @testing-library/user-event
import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
// Query priority (use in this order — higher = more accessible):
// 1. getByRole — by ARIA role (button, heading, textbox, listitem...)
// 2. getByLabelText — form field by its label
// 3. getByPlaceholderText — input by placeholder
// 4. getByText — visible text content
// 5. getByDisplayValue — current form value
// 6. getByAltText — img alt attribute
// 7. getByTitle — title attribute
// 8. getByTestId — data-testid (last resort)
render(<LoginForm />);
// getBy* — throws if not found (use for elements that must exist):
const submit = screen.getByRole("button", { name: /sign in/i });
const email = screen.getByLabelText(/email/i);
const heading = screen.getByRole("heading", { name: "Dashboard", level: 1 });
const listItem = screen.getByRole("listitem", { name: /alice/i });
// queryBy* — returns null if not found (use to assert absence):
const error = screen.queryByRole("alert");
expect(error).not.toBeInTheDocument();
// findBy* — async, waits for element to appear (use for async rendering):
const result = await screen.findByText(/success/i); // waits up to 1s by default
// *All* variants — multiple elements:
const items = screen.getAllByRole("listitem");
expect(items).toHaveLength(3);
2. User Events & Interactions
userEvent vs fireEvent, typing, clicking, selecting, and keyboard navigation
import userEvent from "@testing-library/user-event";
// userEvent simulates real browser events (prefer over fireEvent):
// - fires pointer, mouse, keyboard events in sequence
// - handles focus, blur, change events correctly
const user = userEvent.setup(); // v14+: setup() before rendering
it("submits the login form", async () => {
render(<LoginForm onSubmit={mockSubmit} />);
const user = userEvent.setup();
await user.type(screen.getByLabelText(/email/i), "alice@example.com");
await user.type(screen.getByLabelText(/password/i), "secret123");
await user.click(screen.getByRole("button", { name: /sign in/i }));
expect(mockSubmit).toHaveBeenCalledWith({
email: "alice@example.com",
password: "secret123",
});
});
// Common userEvent methods:
await user.type(input, "hello world"); // types character by character
await user.clear(input); // clears input value
await user.click(element); // click
await user.dblClick(element); // double click
await user.hover(element); // mouse over
await user.unhover(element); // mouse out
await user.tab(); // press Tab key
await user.keyboard("{Enter}"); // press a key
await user.selectOptions(select, "admin"); // <select> option
await user.upload(input, file); // file input
// fireEvent — low level, for edge cases userEvent can't handle:
import { fireEvent } from "@testing-library/react";
fireEvent.change(input, { target: { value: "new value" } });
fireEvent.submit(form);
3. Async Testing & waitFor
waitFor(), findBy queries, act() wrapper, and testing loading/error states
import { render, screen, waitFor } from "@testing-library/react";
// waitFor — retry assertion until it passes (or times out):
it("shows success message after submit", async () => {
render(<ContactForm />);
await userEvent.setup().click(screen.getByRole("button", { name: /submit/i }));
// waitFor retries until assertion passes or 1s timeout:
await waitFor(() => {
expect(screen.getByText(/sent successfully/i)).toBeInTheDocument();
});
});
// findBy* — shorthand for waitFor + getBy:
await screen.findByText(/sent successfully/i); // equivalent to above
// Test loading state:
it("shows loading spinner then data", async () => {
render(<UserList />);
expect(screen.getByRole("progressbar")).toBeInTheDocument(); // loading
await screen.findByText("Alice"); // data loaded
expect(screen.queryByRole("progressbar")).not.toBeInTheDocument();
});
// Test error state:
it("shows error message on fetch failure", async () => {
server.use(http.get("/api/users", () => HttpResponse.error())); // MSW
render(<UserList />);
await screen.findByRole("alert");
expect(screen.getByRole("alert")).toHaveTextContent(/failed to load/i);
});
// Avoid act() warning — always await user events and async assertions:
// WRONG (causes act() warning):
userEvent.click(button); // not awaited
expect(result).toBeInTheDocument();
// CORRECT:
await user.click(button);
await screen.findByText("result");
4. Custom Render, Providers & MSW
Custom render with context providers, testing with React Router, and MSW API mocking
// Custom render — wrap with providers:
import { render, RenderOptions } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { MemoryRouter } from "react-router-dom";
function renderWithProviders(ui: React.ReactElement, options?: RenderOptions) {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } } // no retries in tests
});
return render(
<QueryClientProvider client={queryClient}>
<MemoryRouter>
{ui}
</MemoryRouter>
</QueryClientProvider>,
options
);
}
// Use instead of render():
renderWithProviders(<UserProfile userId="1" />);
// MSW (Mock Service Worker) — intercept real HTTP in tests:
// npm install -D msw
import { setupServer } from "msw/node";
import { http, HttpResponse } from "msw";
const server = setupServer(
http.get("/api/users", () =>
HttpResponse.json([{ id: 1, name: "Alice" }])
),
http.post("/api/users", async ({ request }) => {
const body = await request.json();
return HttpResponse.json({ id: 2, ...body }, { status: 201 });
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers()); // reset per-test overrides
afterAll(() => server.close());
// Override per test:
server.use(http.get("/api/users", () => HttpResponse.error()));
5. Accessibility Queries & Best Practices
Role-based queries, accessible form fields, common mistakes, and debug tools
// Accessible queries enforce good markup:
// getByRole("button") only matches elements with role=button (or <button>)
// If your markup lacks proper roles, RTL queries help you find it
// Common ARIA roles:
// button, link, heading, textbox, checkbox, radio, combobox, listbox
// dialog, alertdialog, alert, status, progressbar, tab, tabpanel
// list, listitem, table, row, cell, columnheader
// Role with name (accessible name from label, aria-label, or text content):
getByRole("button", { name: "Delete" }); // matches: <button>Delete</button>
getByRole("button", { name: /delete/i }); // case-insensitive
getByRole("heading", { level: 2 }); // h2 specifically
getByRole("checkbox", { checked: true }); // checked state
getByRole("option", { selected: true }); // selected option
// Testing forms the right way:
// Use getByLabelText — ensures the label IS connected to the input
<label htmlFor="email">Email</label> // RTL finds input by this label text
<input id="email" type="email" />
// Debug: print the DOM when a query fails:
screen.debug(); // prints entire rendered DOM
screen.debug(screen.getByRole("form")); // prints specific element
// within() — scope queries to a container:
import { within } from "@testing-library/react";
const dialog = screen.getByRole("dialog");
const deleteBtn = within(dialog).getByRole("button", { name: /delete/i });
// Avoids finding the wrong button when multiple exist on page
// Common mistakes:
// ❌ getByTestId — tests implementation, not user-visible behavior
// ❌ .container.querySelector(".my-class") — internal selector
// ✅ getByRole / getByLabelText — what the user sees and interacts with
Track Node.js and testing toolchain releases at ReleaseRun. Related: Jest Reference | Vitest Reference | Playwright Reference | React EOL Tracker
🔍 Free tool: npm Package Health Checker — check @testing-library packages for known CVEs, EOL status, and whether they’re actively maintained.
Founded
2023 in London, UK
Contact
hello@releaserun.com