---
name: react-testing
description: >-
  Write React and React Native tests with Jest and Testing Library that
  survive refactors: behaviour-based assertions, accessible queries,
  network-boundary mocking, and proper async handling. Use when adding or
  reviewing tests.
argument-hint: "[component or flow]"
license: MIT
metadata:
  author: John Felix Lim
  version: "1.0.0"
---
# Testing

Apply when writing or reviewing tests. The guiding question is whether the test would still pass after a refactor that preserved behaviour — and still fail if the behaviour broke.

## Rules

1. **Test behaviour, not implementation** — A test that asserts on state variables or internal method calls fails every time you refactor and passes when the feature is broken. Assert on what the user can observe.
2. **Query the way a user finds things** — Prefer role, label, and text over test IDs. It keeps tests readable and quietly enforces accessibility — an element you cannot query by role is usually one a screen reader cannot find either.
3. **Mock at the network boundary, not the module** — Mocking your own data layer means the test passes even when that layer is broken. Intercept HTTP instead, and the test exercises everything you actually wrote.
4. **Cover the paths that hurt, not the lines that are easy** — Coverage percentage is a poor target — it rewards testing getters. Aim at money paths, auth, offline behaviour, and every bug that reached production once.
5. **Every bug fix ships with a failing-first test** — Write the test, watch it fail, then fix. Without the failing step you have no evidence the test would ever catch a regression.

## Patterns

### Behaviour over internals

**Avoid** — Coupled to internal naming. Breaks on refactor, silent when the UI is actually broken.

```tsx
it("sets loading state", () => {
  const { result } = renderHook(() => useCheckout());
  act(() => result.current.submit());
  // Asserting on a variable name. Rename it and this
  // fails, while the user-visible behaviour is unchanged.
  expect(result.current.isLoading).toBe(true);
});
```

**Prefer** — Survives any refactor that preserves behaviour, and fails when the experience genuinely regresses.

```tsx
it("shows a spinner while the order submits", async () => {
  render(<CheckoutScreen />);

  fireEvent.press(screen.getByRole("button", { name: /place order/i }));

  // What the user actually experiences.
  expect(await screen.findByRole("progressbar")).toBeVisible();
  expect(screen.getByRole("button", { name: /place order/i })).toBeDisabled();
});
```

### Where to mock

**Avoid** — The mock replaces the code under test, so the suite stays green while the real path is broken.

```ts
jest.mock("../api/orders", () => ({
  postOrder: jest.fn().mockResolvedValue({ id: "1" }),
}));
// Your request building, serialization, and error mapping
// are all mocked away. The test proves almost nothing.
```

**Prefer** — Everything you wrote runs. Only the network is faked, and failure cases are trivial to express.

```ts
const server = setupServer(
  http.post("/api/orders", () => HttpResponse.json({ id: "1" })),
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

// Error paths become a one-line override:
server.use(
  http.post("/api/orders", () => new HttpResponse(null, { status: 500 })),
);
```

### Async assertions

**Avoid** — A sleep is a flaky test with a timer. Too short and it fails in CI; too long and the suite crawls.

```tsx
fireEvent.press(submitButton);
await new Promise((r) => setTimeout(r, 1000));  // hope
expect(screen.getByText("Order placed")).toBeTruthy();
```

**Prefer** — Resolves as soon as the condition is met, and fails fast with a useful message when it isn't.

```tsx
fireEvent.press(submitButton);

// Retries until it appears or the timeout is genuinely exceeded.
expect(await screen.findByText("Order placed")).toBeVisible();
```

## Review checklist

- [ ] No test asserts on internal state or private methods.
- [ ] Queries use role, label, or text before test IDs.
- [ ] HTTP is mocked at the boundary; your own modules are not.
- [ ] No arbitrary `setTimeout` waits — `findBy*` and `waitFor` only.
- [ ] Every production bug gained a test that failed before the fix.
- [ ] Critical paths — auth, payment, offline — are covered end to end.

---

From the John Felix Lim Frontend Engineering Playbook — https://github.com/JohnFelixLim