Test Strategy
Purpose
Follow the Testing Trophy: write integration tests mostly, supported by unit tests for pure logic and minimal E2E for critical journeys. Establish CI thresholds so coverage doesn't silently degrade.
Universal — the Testing Trophy ROI model and 3-handler-per-endpoint convention apply to any framework; only the test runner and component-rendering library differ.
Procedure
Run coverage analysis (validation loop)
- Run the test runner with coverage flag
- If branch coverage < target, identify 0%-coverage business logic and add tests; re-run until target met
- Prioritize untested business logic over framework code
- Coverage is a floor, not a goal — 90% coverage with implementation-coupled assertions gives false confidence. Chase untested behavior, not the number
Unit tests — pure functions & custom hooks
- AAA pattern (Arrange / Act / Assert), one assertion focus per test
- Mock only at system boundaries — never mock business logic (runner — see Implementation; Vitest)
Integration tests — the bulk
- Render the component with its real children; mock at the network boundary (not component-level mocks)
- Project convention: 3 handlers per endpoint — happy / error / empty. (A project rule, not from Kent's article — it operationalizes the integration-heavy stance.)
- Assert behavior, not implementation (applies to all layers): query user-visible output by role/label; never assert internal state, props, or that a function was called. A test that breaks on a refactor — when behavior didn't change — is testing the wrong thing
- Query priority
getByRole > getByLabelText > text > getByTestId doubles as an a11y signal: if you can't query by role, the component is probably inaccessible
- Watch for mock drift: a hand-written MSW response can diverge from the real API shape (mock returns X, prod returns Y) → type the handlers against the real schema, or the test passes while prod breaks
- User-centric assertions (query by role, simulate real user events) (RTL + MSW — see Implementation)
E2E — critical user journeys only
- Login → core action → result; limit to journeys where regression would block users
- Reserve E2E for what integration can't reach: real navigation/redirects, auth flows, third-party iframes, multi-tab
- Don't replicate integration tests at the E2E layer (Playwright — see Implementation)
- Visual regression is a separate category role-query tests can't catch (CSS/layout breaks) — Playwright screenshots or Chromatic on critical pages
Component-level a11y checks
- Every shared component passes an a11y check in the component workshop
- Block PR on a11y violations (Storybook a11y addon — see Implementation)
Keep tests deterministic (flake kills suites)
- No real time: fake timers (
vi.useFakeTimers) for debounce/throttle/intervals; never an arbitrary setTimeout/sleep to "wait for" something
- Await async properly —
findBy* / waitFor (not getBy* before data resolves); userEvent is async, await it
- Control randomness, dates, and network: stub
Math.random, freeze Date.now, mock every request (a test that hits the real network is flaky by definition)
- Tests pass in isolation and in any order — no shared mutable state between tests
CI configuration
- Set branch coverage threshold (not line coverage — line coverage rewards trivial getter tests)
- Run unit + integration on every PR; E2E on main only (or with
[e2e] label)
Explicit non-targets
- Document what you intentionally don't test (framework internals, third-party library behavior)
- Prevents test-coverage gaming
Severity tiers
| Tier |
Examples |
Action SLA |
| Critical |
0% coverage on auth / payment / data-integrity paths; no E2E for the primary user journey; tests fail intermittently |
Block release; fix immediately |
| Major |
Business-logic coverage < 50%; missing error/empty MSW handlers; tests assert implementation details (break on refactor); no Storybook a11y on shared components |
Fix this sprint |
| Minor |
Utility coverage < 80%; missing CI flake detection (--repeat-each); no visual-regression on critical pages; unused test helpers |
Schedule within 2 sprints |
Default coverage target if no project-specific target is set: branch coverage ≥ 70% overall; ≥ 90% for security-critical paths.
Completion Criteria
Output
- Test files: organized as
*.test.ts (unit) / *.test.tsx (integration with RTL) / e2e/*.spec.ts (Playwright)
- MSW handlers:
src/mocks/handlers.ts exporting one handler per endpoint (happy / error / empty variants)
- Coverage report: generated by test runner; CI fails if below threshold (default 70% branch)
- CI config:
.github/workflows/ci.yml includes vitest run --coverage with threshold + playwright test for critical journeys
- Commit format:
test(<scope>): <description> for test additions; fix(<scope>): <description> + test for regression-driven additions
Implementation
React + Next.js (default)
- Runner: Vitest (
vitest run --coverage)
- Component rendering: React Testing Library (RTL) —
screen.getByRole, userEvent
- Network mocking: MSW (Mock Service Worker)
- E2E: Playwright (
npx playwright test --repeat-each=10 for flake detection)
- Determinism:
vi.useFakeTimers() for timers; await userEvent.*; findBy*/waitFor for async; stub Math.random / vi.setSystemTime
- Visual regression: Playwright
expect(page).toHaveScreenshot() or Chromatic (Storybook)
- Storybook a11y addon for component-level a11y checks
Other stacks
- Vue / Nuxt: Vitest + Vue Testing Library (
@testing-library/vue); MSW works identically; Playwright for E2E
- SvelteKit: Vitest +
@testing-library/svelte; MSW or Vitest's built-in vi.fn() for mocks; Playwright for E2E (built into SvelteKit's default template)
- Angular: Jest or Vitest + Angular Testing Library (
@testing-library/angular); MSW for HTTP mocks; Playwright or Cypress for E2E
- Universal: Testing Trophy / Testing Pyramid logic is framework-agnostic; MSW intercepts at the network layer regardless of client; Playwright drives the browser regardless of framework
Related skills
component-quality — extracted hooks/components need accompanying tests
cicd-pipeline — wire coverage threshold + Playwright into the GitHub Actions matrix
accessibility-audit — Storybook a11y addon runs as part of the test pipeline
Reference
- Key insight encoded: "Write tests. Not too many. Mostly integration." Center the stack on RTL + MSW integration tests. Reserve Playwright for critical user journeys — Vitest Browser Mode now closes the gap for component-level needs in 2025+, so E2E weight should stay light. Quality beats quantity: assert user-visible behavior (so tests survive refactors), not implementation; coverage is a floor, not a goal; and a flaky suite (real timers/network/random) gets ignored — keep tests deterministic.
- Tool substitution note: Kent's original article cites Jest + Cypress. The substitution (Vitest for Jest, Playwright for Cypress, MSW for fetch mocks) is the modern equivalent at the time of writing, not Kent's literal recommendation. The 3-handler-per-endpoint convention is also a project rule, not from the article.
1---2name: test-strategy-23description: Apply the Testing Trophy (mostly integration tests with RTL + MSW, sparing E2E with Playwright) and set coverage thresholds. Use before new feature work, after bug fixes, when CI coverage falls below target, or when tests are flaky or break on every refactor. Not for wiring coverage gates + Playwright into the GitHub Actions matrix (use cicd-pipeline) or auditing WCAG a11y compliance (use accessibility-audit).4license: MIT5---67# Test Strategy89## Purpose10Follow the Testing Trophy: write *integration tests mostly*, supported by unit tests for pure logic and minimal E2E for critical journeys. Establish CI thresholds so coverage doesn't silently degrade.1112**Universal** — the Testing Trophy ROI model and 3-handler-per-endpoint convention apply to any framework; only the test runner and component-rendering library differ.1314## Procedure15161. **Run coverage analysis (validation loop)**17 - Run the test runner with coverage flag18 - If branch coverage < target, identify 0%-coverage business logic and add tests; re-run until target met19 - Prioritize untested business logic over framework code20 - **Coverage is a floor, not a goal** — 90% coverage with implementation-coupled assertions gives false confidence. Chase untested *behavior*, not the number21222. **Unit tests — pure functions & custom hooks**23 - AAA pattern (Arrange / Act / Assert), one assertion focus per test24 - Mock only at system boundaries — never mock business logic *(runner — see Implementation; Vitest)*25263. **Integration tests — the bulk**27 - Render the component with its real children; mock at the **network boundary** (not component-level mocks)28 - **Project convention: 3 handlers per endpoint — happy / error / empty.** (A project rule, not from Kent's article — it operationalizes the integration-heavy stance.)29 - **Assert behavior, not implementation** (applies to all layers): query user-visible output by role/label; never assert internal state, props, or that a function was called. A test that breaks on a refactor — when behavior didn't change — is testing the wrong thing30 - Query priority `getByRole` > `getByLabelText` > text > `getByTestId` doubles as an a11y signal: if you can't query by role, the component is probably inaccessible31 - **Watch for mock drift**: a hand-written MSW response can diverge from the real API shape (mock returns X, prod returns Y) → type the handlers against the real schema, or the test passes while prod breaks32 - User-centric assertions (query by role, simulate real user events) *(RTL + MSW — see Implementation)*33344. **E2E — critical user journeys only**35 - Login → core action → result; limit to journeys where regression would block users36 - Reserve E2E for what integration can't reach: real navigation/redirects, auth flows, third-party iframes, multi-tab37 - Don't replicate integration tests at the E2E layer *(Playwright — see Implementation)*38 - **Visual regression** is a separate category role-query tests can't catch (CSS/layout breaks) — Playwright screenshots or Chromatic on critical pages39405. **Component-level a11y checks**41 - Every shared component passes an a11y check in the component workshop42 - Block PR on a11y violations *(Storybook a11y addon — see Implementation)*43446. **Keep tests deterministic (flake kills suites)**45 - No real time: fake timers (`vi.useFakeTimers`) for debounce/throttle/intervals; never an arbitrary `setTimeout`/sleep to "wait for" something46 - Await async properly — `findBy*` / `waitFor` (not `getBy*` before data resolves); `userEvent` is async, await it47 - Control randomness, dates, and network: stub `Math.random`, freeze `Date.now`, mock every request (a test that hits the real network is flaky by definition)48 - Tests pass in isolation and in any order — no shared mutable state between tests49507. **CI configuration**51 - Set branch coverage threshold (not line coverage — line coverage rewards trivial getter tests)52 - Run unit + integration on every PR; E2E on main only (or with `[e2e]` label)53548. **Explicit non-targets**55 - Document what you intentionally don't test (framework internals, third-party library behavior)56 - Prevents test-coverage gaming5758## Severity tiers5960| Tier | Examples | Action SLA |61|---|---|---|62| **Critical** | 0% coverage on auth / payment / data-integrity paths; no E2E for the primary user journey; tests fail intermittently | Block release; fix immediately |63| **Major** | Business-logic coverage < 50%; missing error/empty MSW handlers; tests assert implementation details (break on refactor); no Storybook a11y on shared components | Fix this sprint |64| **Minor** | Utility coverage < 80%; missing CI flake detection (`--repeat-each`); no visual-regression on critical pages; unused test helpers | Schedule within 2 sprints |6566**Default coverage target if no project-specific target is set**: branch coverage ≥ 70% overall; ≥ 90% for security-critical paths.6768## Completion Criteria69- [ ] Branch coverage ≥ target (default ≥ 70% if unset; ≥ 90% on critical paths)70- [ ] Tests assert user-visible behavior, not implementation details (survive a refactor)71- [ ] Test suite is deterministic — no real timers/network/random; passes under `--repeat-each`72- [ ] Critical user journeys covered by E2E73- [ ] MSW handlers cover happy / error / empty per endpoint74- [ ] CI passes; no skipped tests left in main75- [ ] Storybook a11y addon = 0 violations on shared components76- [ ] All Critical findings fixed; all Major findings scheduled7778## Output79- **Test files**: organized as `*.test.ts` (unit) / `*.test.tsx` (integration with RTL) / `e2e/*.spec.ts` (Playwright)80- **MSW handlers**: `src/mocks/handlers.ts` exporting one handler per endpoint (happy / error / empty variants)81- **Coverage report**: generated by test runner; CI fails if below threshold (default 70% branch)82- **CI config**: `.github/workflows/ci.yml` includes `vitest run --coverage` with threshold + `playwright test` for critical journeys83- **Commit format**: `test(<scope>): <description>` for test additions; `fix(<scope>): <description> + test` for regression-driven additions8485## Implementation8687### React + Next.js (default)88- Runner: Vitest (`vitest run --coverage`)89- Component rendering: React Testing Library (RTL) — `screen.getByRole`, `userEvent`90- Network mocking: MSW (Mock Service Worker)91- E2E: Playwright (`npx playwright test --repeat-each=10` for flake detection)92- Determinism: `vi.useFakeTimers()` for timers; `await userEvent.*`; `findBy*`/`waitFor` for async; stub `Math.random` / `vi.setSystemTime`93- Visual regression: Playwright `expect(page).toHaveScreenshot()` or Chromatic (Storybook)94- Storybook a11y addon for component-level a11y checks9596### Other stacks97- **Vue / Nuxt**: Vitest + Vue Testing Library (`@testing-library/vue`); MSW works identically; Playwright for E2E98- **SvelteKit**: Vitest + `@testing-library/svelte`; MSW or Vitest's built-in `vi.fn()` for mocks; Playwright for E2E (built into SvelteKit's default template)99- **Angular**: Jest or Vitest + Angular Testing Library (`@testing-library/angular`); MSW for HTTP mocks; Playwright or Cypress for E2E100- **Universal**: Testing Trophy / Testing Pyramid logic is framework-agnostic; MSW intercepts at the network layer regardless of client; Playwright drives the browser regardless of framework101102## Related skills103- `component-quality` — extracted hooks/components need accompanying tests104- `cicd-pipeline` — wire coverage threshold + Playwright into the GitHub Actions matrix105- `accessibility-audit` — Storybook a11y addon runs as part of the test pipeline106107## Reference108- **Key insight encoded**: "Write tests. Not too many. Mostly integration." Center the stack on RTL + MSW integration tests. Reserve Playwright for critical user journeys — Vitest Browser Mode now closes the gap for component-level needs in 2025+, so E2E weight should stay light. Quality beats quantity: assert user-visible *behavior* (so tests survive refactors), not implementation; coverage is a floor, not a goal; and a flaky suite (real timers/network/random) gets ignored — keep tests deterministic.109- **Tool substitution note**: Kent's original article cites Jest + Cypress. The substitution (Vitest for Jest, Playwright for Cypress, MSW for fetch mocks) is the modern equivalent at the time of writing, not Kent's literal recommendation. The 3-handler-per-endpoint convention is also a project rule, not from the article.