Topics in this subject
React 4 min read Updated 5 Aug 2026

Testing

React Testing Library philosophy, queries by role/text, userEvent, async findBy, and mocking fetch.

Philosophy — test behavior, not implementation#

React Testing Library's guiding principle: the more your tests resemble how users use your software, the more confidence they give. You interact with the rendered output (roles, text, labels) instead of poking at component internals (state, instance methods, class names).

🟢 Do: assert what the user sees and can do. ⚠️ Don't: assert useState values, call instance methods, or query by CSS class / test-only structure. Those break on refactors that don't change behavior.

flowchart LR
  A["render(<Component/>)"] --> B["query via screen (role/text/label)"]
  B --> C["interact via userEvent"]
  C --> D["assert on the DOM (jest-dom matchers)"]

Core API#

Tool Purpose
render Mount the component into a JSDOM container
screen Global query object bound to the document
userEvent Realistic user interactions (clicks, typing, tab)
getBy* Sync, element must exist (throws if not)
queryBy* Sync, returns null if absent (assert non-existence)
findBy* Async, retries until it appears (returns a Promise)
*AllBy* Return arrays for multiple matches

Query priority: prefer getByRole (accessible, mirrors assistive tech), then getByLabelText (forms), getByText, and only fall back to getByTestId when nothing semantic fits.

Example test#

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import Counter from "./Counter";

test("increments when the button is clicked", async () => {
  const user = userEvent.setup();
  render(<Counter />);

  // query by accessible role + name
  const button = screen.getByRole("button", { name: /count: 0/i });
  await user.click(button);

  expect(screen.getByRole("button", { name: /count: 1/i })).toBeInTheDocument();
});

Async & findBy#

Use findBy* (or waitFor) for anything that appears after a promise resolves — data fetches, transitions. findBy polls until the element shows up or times out, so you avoid manual delays.

test("shows the user after fetch resolves", async () => {
  render(<Profile id="42" />);

  // spinner first
  expect(screen.getByText(/loading/i)).toBeInTheDocument();

  // findBy waits for the async result
  expect(await screen.findByRole("heading", { name: /ada lovelace/i }))
    .toBeInTheDocument();
});

⚠️ getBy after an async action fails because it doesn't wait — use findBy. Wrap manual state changes in act only when RTL doesn't already (it wraps render/userEvent for you).

Mocking fetch#

Mock the network at the boundary. Two common approaches: stub global.fetch, or use MSW (Mock Service Worker) to intercept requests at the network layer — closer to reality and reusable across tests.

// Simple fetch stub
beforeEach(() => {
  global.fetch = jest.fn(() =>
    Promise.resolve({ ok: true, json: () => Promise.resolve({ name: "Ada Lovelace" }) })
  );
});
afterEach(() => jest.restoreAllMocks());
// MSW (preferred for realism)
import { setupServer } from "msw/node";
import { http, HttpResponse } from "msw";

const server = setupServer(
  http.get("/api/users/:id", () => HttpResponse.json({ name: "Ada Lovelace" }))
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

Jest vs Vitest#

Jest Vitest
Ecosystem Mature, default in CRA/Next Vite-native, fast
Config Own transform (babel/swc) Reuses Vite config/ESM
API jest.fn, jest.mock Near drop-in (vi.fn, vi.mock)
Speed Good Faster HMR/watch

Both pair with @testing-library/react and @testing-library/jest-dom (matchers like toBeInTheDocument, toHaveValue). Vitest is the common choice for Vite projects; Jest remains standard elsewhere. 🎯

Interview Q&A#

Q1. What's the core philosophy of React Testing Library? Test components the way users experience them — query by role/text/label and interact via events — rather than asserting internal state or implementation details, so tests survive refactors.

Q2. getBy vs queryBy vs findBy? getBy returns synchronously and throws if absent; queryBy returns null (use it to assert absence); findBy returns a promise that retries until the element appears — for async UI.

Q3. Why prefer getByRole over getByTestId? getByRole reflects the accessibility tree, so tests double as a11y checks and resemble real usage. getByTestId couples tests to markup and is a last resort when no semantic query fits.

Q4. Why not assert on component state? State is an implementation detail; a refactor can change it without changing behavior, giving false failures. Asserting on rendered output gives confidence in what users actually get.

Q5. How do you handle network calls in tests? Mock at the boundary — stub global.fetch or, preferably, use MSW to intercept requests at the network layer for realism and reuse — then assert the resulting UI with findBy.