Frontend Testing Strategy

FE Lead · Lesson 21 — print before the round
One-line to open with

"The best ROI for UI is integration tests — they test what the user experiences, not how you built it. I use unit tests for pure logic, integration for component behaviour, e2e sparingly for critical journeys. And I measure escaped defects, not coverage %."

Testing trophy (UI ROI order)

e2e — few · Playwright sharded
Integration — most · RTL + MSW · highest ROI
Unit — utilities + pure logic · Vitest/Jest
Static analysis — cheapest · TS + ESLint

Unit tests of React components test implementation details → break on refactor without catching bugs. Integration tests test behaviour the user sees.

Testing Library query priority

#QueryWhy
1getByRoleARIA semantics — tests a11y too
2getByLabelTextForm inputs with labels
3getByTextVisible text content
4getByTestIdLast resort — not user-visible

Async query families

  • getBy* — sync, throws immediately if absent
  • findBy*always use after async ops (fetches, state updates)
  • queryBy* — sync, returns null (use to assert absence)

userEvent over fireEvent — fires the full browser event sequence (pointerdown→mousedown→focus→click), catches more real bugs.

Tooling choices

NeedTool
RunnerVitest (ESM-native, fast) / Jest
UI interactionReact Testing Library
Network mockMSW — network-level, lib-agnostic
e2ePlaywright (multi-browser, sharded)
Visual regressionChromatic (Storybook cloud) / Percy / Playwright toHaveScreenshot()

MSW > axios mock: intercepts at network level, works with any fetch lib, same handlers in tests AND browser.

Visual regression

Screenshot diff on every PR — catches unintended CSS changes (spacing, colour, layout). Storybook play() = document + interact + visual regression from one story.

Does NOT catch functional bugs (behaviour, wrong data)

Catches unintended CSS regressions: spacing, font weight, colour, layout shift

Trap: "accept all" habit. Scope to shared primitives only. Make the designer (not engineer) the approver. Keep it informational — not a hard gate.

Flakiness root causes + fixes

CauseFix
Timingawait findBy* not getBy* for async
Shared stateCleanup in afterEach; RTL auto-cleanup
Date/timevi.useFakeTimers() + reset in afterEach
Real networkMSW in unit/integration; route.fulfill() in e2e
AnimationsDisable in test env via prefers-reduced-motion
Fragile selectorsARIA roles; data-testid as last resort

Fleet mgmt: quarantine (skip + ticket) → sprint dedicate 20% to fixes → track flakiness rate (target <2%).

Culture playbook

  1. Shared test-utils pkg (RTL + MSW pre-configured)
  2. Pair on first test per team (remove blank-page problem)
  3. Floor gate (60–70%) — prevent zero-coverage features
  4. Track escaped defects — can't be gamed unlike coverage %
  5. Celebrate test coverage in PR review
Memorize: the lead answer to "how do you test at scale"

Testing trophy → RTL+MSW integration tests for most UI → Playwright sharded for critical journeys → quarantine-not-retry for flakiness → shared test-utils package → escaped defects as the quality metric. "I make tests easy to write, then make them the default."

Don't fail the interview

Retries ≠ fix: CI retries hide the root cause. Quarantine flaky tests (skip + ticket), diagnose (timing? shared state? real network?), then fix. A retry count >1 is a signal to investigate, not a success metric.

100% coverage ≠ quality: render(<Component />) without assertions gives line coverage with zero confidence. Track escaped defects instead. Set a low floor gate; don't chase a ceiling.