Skip to content
Testing

Cross-cutting Quality

Listen 0%
Speed

16 · Testing

A frontend testing strategy that gives confidence without grinding CI to a halt: the testing trophy, unit/integration/E2E layers, Testing Library philosophy, contract and visual testing. Current as of Vitest 3 (Vite-native, the modern default) and Playwright as the dominant E2E tool.


Positioning

Tests exist to give you confidence to change code, not to hit a coverage number. The senior skill is allocating test effort where it buys the most confidence per unit of maintenance cost — which on the frontend means favoring tests that exercise behavior the way users do, and being ruthless about flaky or implementation-coupled tests. The shape of the modern frontend test suite is the testing trophy, not the classic pyramid.


Foundations

Pyramid vs Trophy

  • Test Pyramid (Mike Cohn): many unit, fewer integration, few E2E. Correct for backends; on the frontend its “many isolated unit tests” emphasis leads to brittle, low-confidence suites that test implementation details.
  • Testing Trophy (Kent C. Dodds): small base of static analysis (TS, ESLint), a layer of unit tests, a large middle of integration tests (the highest confidence-per-cost on the frontend), and a thin top of E2E. The guiding principle: “The more your tests resemble the way your software is used, the more confidence they give you.”

The four layers

  1. Static — TypeScript (03) + ESLint catch a whole class of bugs before any test runs. The cheapest “tests” you have.
  2. Unit — pure functions, hooks, domain logic (10), reducers. Fast, deterministic, great for algorithmic/business logic. Don’t unit-test trivial components.
  3. Integration — a component (or a few) rendered together with realistic data, interacted with as a user would. The sweet spot — high confidence, reasonable cost. This is where Testing Library lives.
  4. E2E — the real app in a real browser across real flows (login, checkout). Highest confidence, highest cost/flakiness; reserve for critical revenue paths.

Deep dive: the 2026 toolchain

Vitest — the modern unit/integration runner

Vitest 3 is the Vite-native test runner and the default for new projects: native ESM, TypeScript, and JSX with no separate compile step, a Jest-compatible API (migration is largely search-and-replace), and 2–4× (often more) faster than Jest on Vite projects. It shares your Vite config/transform pipeline, so tests run the same way your app builds. Browser Mode (via Playwright, Chromium-first) renders components in a real browser for higher-fidelity component tests. Jest remains fine for existing, stable setups (especially non-Vite/Webpack projects) — no urgent reason to migrate, but don’t pick it for greenfield.

Testing Library — the philosophy, not just a tool

@testing-library/react (and the DOM/user-event packages) enforces user-centric testing: query by accessible role/label/text (what a user/screen reader perceives), not by test IDs or component internals. userEvent simulates real interaction sequences. This makes tests resilient to refactors (you can rewrite the component internals without breaking tests) and doubles as an accessibility check (19) — if you can’t query by role, your markup is probably inaccessible. Avoid container.querySelector, snapshot-everything, and testing state/props directly.

Playwright — the dominant E2E tool

Cross-browser (Chromium/Firefox/WebKit), fast, with auto-waiting (no manual sleeps), tracing/time-travel debugging, parallelism/sharding, network interception, and component-testing support. The default for new teams in 2026; Cypress is still actively developed and fine where already adopted, but new projects pick Playwright. Key in the RSC era: Vitest currently can’t render async Server Components, so async RSC, auth flows, and full-stack paths are pushed to Playwright — your tool choice is partly forced by what can render where.

Mocking the network — MSW

Mock Service Worker intercepts at the network layer (Service Worker in browser, request interception in Node), so your components/app make real fetch calls against mock handlers. Far more realistic than mocking fetch/axios modules, and the same handlers work in unit, integration, Storybook, and E2E. The modern default for API mocking.

Contract testing — Pact

For MFEs/microservices (09, 13), consumer-driven contract testing (Pact) verifies that the frontend’s expectations of an API and the backend’s actual responses stay compatible — catching integration breaks without slow, flaky end-to-end environments. Each side tests against the shared contract independently.

Visual regression & component workshop

  • Storybook (11) — render components in isolation across states; run interaction tests (play functions) and an a11y addon (19).
  • Visual regression — Chromatic, Playwright screenshots, Percy, or Loki diff rendered output against baselines to catch unintended visual changes (critical for design systems).

Worked example: an integration test (Vitest + Testing Library + MSW)

// SearchUsers.test.tsx — test behavior the way a user experiences it
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { http, HttpResponse } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
  http.get('/api/users', ({ request }) => {
    const q = new URL(request.url).searchParams.get('q');
    return HttpResponse.json(q === 'ann' ? [{ id: 1, name: 'Ann' }] : []);
  }),
);
beforeAll(() => server.listen()); afterEach(() => server.resetHandlers()); afterAll(() => server.close());

test('shows matching users as you type', async () => {
  render(<SearchUsers />);
  await userEvent.type(screen.getByRole('searchbox', { name: /search users/i }), 'ann');
  // query by accessible role/text, assert on user-visible outcome — not internal state
  expect(await screen.findByRole('listitem', { name: /ann/i })).toBeVisible();
});

No implementation details, real network shape via MSW, queried by accessible role (so it also asserts a11y). This single test gives more confidence than a dozen shallow unit tests of the component’s internals.


Pitfalls & gotchas

  • Testing implementation details (state, props, internal functions) → brittle tests that break on refactor and give false confidence. Test behavior.
  • Over-mocking (mocking fetch/modules instead of MSW) → tests pass while the real integration is broken.
  • Snapshot-everything → giant snapshots nobody reads; reviewers rubber-stamp diffs.
  • Too many E2E tests → slow, flaky CI; reserve E2E for critical paths.
  • Retrying flaky tests instead of fixing them → erodes trust; quarantine and fix root cause (timing, shared state, network noise).
  • getByTestId everywhere → bypasses the accessibility-query benefit; prefer role/label.
  • Coverage as a goal → 100% of trivial code, 0 confidence in hard paths.
  • Forgetting async RSC can’t render in Vitest → push those to Playwright.

Interview questions

  1. Pyramid vs trophy — why does the frontend favor the trophy?
  2. What is Testing Library’s core philosophy, and why query by role?
  3. Vitest vs Jest in 2026 — when each?
  4. Why is MSW better than mocking fetch?
  5. Unit vs integration vs E2E — what does each buy you, at what cost?
  6. When do you reach for Playwright over Vitest? (Async RSC, real flows, cross-browser.)
  7. What is consumer-driven contract testing and when do you need it?
  8. How do you handle a flaky test?
  9. How does Testing Library overlap with accessibility?
  10. What’s wrong with chasing 100% coverage?

Recommendations

  • Shape the suite as a trophy: lean on TS + ESLint, write mostly integration tests, keep E2E thin on critical paths.
  • Use Vitest 3 + Testing Library + user-event for unit/integration; Playwright for E2E and async-RSC/full-stack flows.
  • Mock the network with MSW, reusing handlers across unit/integration/Storybook.
  • Query by accessible role/label; never test implementation details.
  • Add visual regression for the design system (11) and contract tests (Pact) for MFE/service boundaries.
  • Fix or quarantine flaky tests; don’t paper over with retries.
  • Test the risky/complex code well; ignore coverage of the trivial.

Books & references

  • “Testing JavaScript” course + Epic Web / Epic React — Kent C. Dodds (testingjavascript.com, epicweb.dev). The trophy, Testing Library, and the modern philosophy.
  • Testing Library docs (testing-library.com) — guiding principles + query priority order. Read “Common Mistakes with React Testing Library.”
  • Vitest docs (vitest.dev) — runner config, Browser Mode, migration from Jest.
  • Playwright docs (playwright.dev) — auto-waiting, tracing, best practices, component testing.
  • MSW docs (mswjs.io) — network-level mocking.
  • Pact docs (docs.pact.io) — consumer-driven contract testing.
  • “Working Effectively with Legacy Code” — Michael Feathers. Seams and getting untested code under test.

Connections

Frontend Deep-Dive Library · content is the single source of truth.