Write frontend tests
Two layers; pick by what you're verifying. principle → ▸ Example (Jest/RTL/Cypress) → ▸ Other stacks.
| Layer |
Tool |
For |
| Unit / component |
Jest + React Testing Library |
utils, hooks, component behavior in isolation |
| E2E |
Cypress + Cucumber (BDD) |
real user flows through the running app |
0. Gate — does this deserve a test? (test behaviors, not files)
A suite maps to behaviors/contracts, not the file tree. Never create a test file 1:1 with a
source file by default — per-util mirror tests (foo.utils.ts → foo.utils.test.ts) bury real
failures, slow every run, and are maintained forever.
- Direct util/hook test only when the logic is genuinely complex AND matters (money, dates,
parsing, non-trivial business rules) or it pins a regression that actually occurred.
- Skip it when a component/page/e2e test already exercises the behavior, or the code is trivial
glue (mapping, prop plumbing, re-exports) whose breakage is instantly visible in dev.
- Prefer proving a util through the component that uses it; a separate util test duplicating
that coverage is junk. When in doubt → don't write it ("Test only what matters",
~/.claude/CLAUDE.md).
1. Unit / component (Jest + RTL)
- Colocate
*.test.ts(x) next to source (or under src/test/). Test behavior through the
public surface: render the component, query by role/text, fire user events, assert what the user
sees — not internal state. Pure utils/hooks that pass the §0 gate are tested directly.
- Mock the framework + externals only:
jest.mock('next/router', () => ({ useRouter: () => ({ query: {} }) })), mock service classes;
never mock the unit under test.
- AAA + specific assertions (
toEqual/toMatchObject with real expected values, not toBeTruthy()).describe('convertObjectToArray', () => {
it('flattens primitive values', () => {
expect(convertObjectToArray({ a: 1, b: 'x' })).toEqual(['a', '1', 'b', 'x']);
});
});
▸ Other stacks: Vitest + RTL — identical patterns.
2. E2E (Cypress + Cucumber)
3. Gates
Lint/format = Biome (warning-first rollout, promoted to errors over time). A git-hook runner
(e.g. lefthook) runs biome check/format on staged files pre-commit, and type-check + jest
pre-push. Run pnpm test (unit) and the e2e suite before opening a PR.
Verification
- Important new behavior is covered at the outermost sensible layer (component via RTL; Cypress for
a user-facing flow); any direct util/hook test passes the §0 gate — no 1:1 mirror files, no
mock-only tests. Tests assert observable behavior with specific values, not truthiness.
pnpm type-check, pnpm test, and Biome all pass.
Related
structure-a-frontend-app · write-frontend-code · write-unit-tests (backend equivalent).
1---2name: write-frontend-tests3description: Use when writing frontend tests — a test-worthiness gate first (no 1:1 per-file test mirroring), then Jest + React Testing Library for components/hooks/utils that earn a test, and Cypress + Cucumber for e2e user flows. Where tests live, what to test, mocking the router, and the lint/type/test gates. React/Next.js reference.4---56# Write frontend tests78Two layers; pick by what you're verifying. principle → **▸ Example (Jest/RTL/Cypress)** → **▸ Other stacks**.910| Layer | Tool | For |11|---|---|---|12| Unit / component | Jest + React Testing Library | utils, hooks, component behavior in isolation |13| E2E | Cypress + Cucumber (BDD) | real user flows through the running app |1415## 0. Gate — does this deserve a test? (test behaviors, not files)16A suite maps to **behaviors/contracts, not the file tree**. Never create a test file 1:1 with a17source file by default — per-util mirror tests (`foo.utils.ts` → `foo.utils.test.ts`) bury real18failures, slow every run, and are maintained forever.19- **Direct util/hook test only when** the logic is genuinely complex AND matters (money, dates,20 parsing, non-trivial business rules) or it pins a regression that actually occurred.21- **Skip it when** a component/page/e2e test already exercises the behavior, or the code is trivial22 glue (mapping, prop plumbing, re-exports) whose breakage is instantly visible in dev.23- Prefer proving a util **through the component that uses it**; a separate util test duplicating24 that coverage is junk. When in doubt → don't write it ("Test only what matters", `~/.claude/CLAUDE.md`).2526## 1. Unit / component (Jest + RTL)27- **Colocate** `*.test.ts(x)` next to source (or under `src/test/`). Test **behavior through the28 public surface**: render the component, query by role/text, fire user events, assert what the user29 sees — not internal state. Pure utils/hooks that pass the §0 gate are tested directly.30- **Mock the framework + externals only:**31 `jest.mock('next/router', () => ({ useRouter: () => ({ query: {} }) }))`, mock service classes;32 never mock the unit under test.33- **AAA + specific assertions** (`toEqual`/`toMatchObject` with real expected values, not `toBeTruthy()`).34 ```ts35 describe('convertObjectToArray', () => {36 it('flattens primitive values', () => {37 expect(convertObjectToArray({ a: 1, b: 'x' })).toEqual(['a', '1', 'b', 'x']);38 });39 });40 ```41 ▸ *Other stacks:* Vitest + RTL — identical patterns.4243## 2. E2E (Cypress + Cucumber)44- **Gherkin feature files** under `cypress/e2e/<feature>/*.feature` describe flows in45 Given/When/Then; **step definitions** implement them; `Scenario Outline` + `Examples` cover input46 variations. Keep **API helpers** in `cypress/services/`, **test data** in `cypress/constants/`,47 and custom commands in `cypress/support/`.48 ```gherkin49 Scenario: Create a listing50 Given User logged in51 When User submits the new-listing form52 Then The listing appears in the list53 ```54 ▸ *Other stacks:* Playwright (+ optional BDD) — same user-flow-level coverage.5556## 3. Gates57Lint/format = **Biome** (warning-first rollout, promoted to errors over time). A git-hook runner58(e.g. lefthook) runs `biome check`/`format` on staged files pre-commit, and `type-check` + `jest`59pre-push. Run `pnpm test` (unit) and the e2e suite before opening a PR.6061## Verification62- Important new behavior is covered at the outermost sensible layer (component via RTL; Cypress for63 a user-facing flow); any direct util/hook test passes the §0 gate — no 1:1 mirror files, no64 mock-only tests. Tests assert observable behavior with specific values, not truthiness.65- `pnpm type-check`, `pnpm test`, and Biome all pass.6667## Related68- `structure-a-frontend-app` · `write-frontend-code` · `write-unit-tests` (backend equivalent).