Testing Standards & Policy
| Field | Value |
|---|---|
| Type | Skill Resource |
| Source | ~/.copilot/skills/quality/references/testing-standards.md |
| Description | Not specified |
Source Content
Testing Standards & Policy
This reference consolidates the testing layer selection, naming conventions, coverage policy, flake elimination, and the what-NOT-to-test list from the absorbed testing-standards skill.
The Testing Pyramid
Test the way the user uses the software, and write the cheapest test that gives that confidence. Push volume down the pyramid; reserve the slow, brittle layers for journeys nothing else can cover.
| Layer | Tool | Tests | Use when |
|---|---|---|---|
| Unit | Vitest | A pure function, reducer, hook, or Go function in isolation | Logic has branches, edge cases, or math — and no DOM or network |
| Component | React Testing Library + userEvent | A component through its public behavior — render, interact, assert | A user can see or do something; covers component + local state together |
| Network | MSW | Component/integration behavior across a mocked network boundary | The unit under test fetches or mutates; mock at the network edge, not the module |
| Accessibility | jest-axe | No WCAG violations in rendered output | Every component test — expect(await axe(container)).toHaveNoViolations() |
| Visual | Lost Pixel | Pixel diffs of stories/pages — never Chromatic | A change could regress layout, spacing, or theme that assertions miss |
| E2E | Project choice (no default) | A full critical journey across real pages | A journey spans routes/auth/persistence; one happy path + one failure path |
Test Naming and Structure
Naming Convention
Tests follow the pattern: it('does X when Y'). The name describes the behavior, not the implementation.
Good examples:
it('shows error message when credentials are invalid')it('adds item to cart when button is clicked')it('returns 0 discount for orders under $50')it('navigates to dashboard after successful login')
Avoid:
it('works')— not specificit('test 1')— not descriptiveit('should do stuff')— too vagueit('renders')— doesn’t describe behavior
Describe Block Organization
Organize describe blocks around units, then contexts or sub-behaviors:
describe('UserService', () => { describe('createUser', () => { describe('with valid input', () => { it('creates user in database', () => {}); it('returns user with id', () => {}); });
describe('with invalid input', () => { it('throws ValidationError for missing email', () => {}); it('throws ValidationError for invalid email format', () => {}); }); });
describe('deleteUser', () => { it('removes user from database', () => {}); });});Arrange-Act-Assert (AAA) Structure
Every test follows the same three-phase structure:
- Arrange — set up test data and conditions
- Act — execute the code being tested
- Assert — verify the outcome
Use blank lines to separate the three phases:
it('calculates total with discount', () => { // Arrange const items = [ { name: 'Widget', price: 100, quantity: 2 }, { name: 'Gadget', price: 50, quantity: 1 }, ]; const discountRate = 0.1;
// Act const result = calculateTotal(items, discountRate);
// Assert expect(result).toBe(225); // (200 + 50) * 0.9});Coverage Policy (the >=50% Line Floor)
The baseline is >=50% line coverage, enforced as one of ADR-005’s release gates — failing it blocks merge. Read the floor as a release minimum on business-critical code, not a target to game.
The Gate
- It is a floor, not a goal. 50% of meaningful branches beats 90% padded with glue.
- It measures lines, the cheapest signal — high line coverage with weak assertions is still weak.
- Direct it at risk. Find uncovered branches in code that breaks badly; ignore coverage on generated, trivial, or glue code.
- Coverage is one gate among several: typecheck, build, lint,
test(>=50%), jest-axe, Lost Pixel, bundle budget, and the docs gate (ADR-005).
Anti-Gaming Rules
What NOT to do:
- Don’t artificially raise coverage by testing trivial pass-throughs.
- Don’t write assertions that don’t actually verify behavior.
- Don’t count generated code (routeTree.gen.ts, codegen output).
- Don’t test the framework or library (React, TanStack, Zod are already tested).
- Don’t test private implementation details if only to hit a number.
Code Types and Coverage Targets
| Code Type | Priority | Coverage Target |
|---|---|---|
| Business logic (state management, API handlers, validation, payment) | High | 80%+ |
| UI components (but not trivial wrappers) | Medium | 60-80% |
| Utility functions and transformers | Medium | 60-80% |
| Custom hooks | Medium | 60-80% |
| Static pages, simple wrappers, types/interfaces | Lower | 40-60% |
| Generated code, configuration files | Skip | — |
Flake Elimination
A flaky test is a defect — quarantine and fix it, never paper over it with a blanket retry-til-green. The four most common causes and their fixes:
1. Timing / Async
Problem: Test doesn’t wait for async operations.
Fix: Use findBy* (waits automatically) or waitFor in assertions; always await userEvent calls.
// Flakyit('shows user name', () => { render(<UserProfile userId="123" />); expect(screen.getByText('John')).toBeInTheDocument(); // May not exist yet});
// Fixedit('shows user name', async () => { render(<UserProfile userId="123" />); await expect(screen.findByText('John')).resolves.toBeInTheDocument();});2. Test-Order Dependency
Problem: Test state leaks from one test to the next; tests pass in one order, fail in another.
Fix: Fresh state for every test — each test sets up and tears down its own data. No shared mutable globals.
// Flakydescribe('Counter', () => { const counter = new Counter(); // Shared!
it('increments', () => { counter.increment(); expect(counter.value).toBe(1); });
it('starts at zero', () => { expect(counter.value).toBe(0); // Fails if this runs after the first test });});
// Fixeddescribe('Counter', () => { let counter: Counter;
beforeEach(() => { counter = new Counter(); // Fresh instance });
it('increments', () => { counter.increment(); expect(counter.value).toBe(1); });
it('starts at zero', () => { expect(counter.value).toBe(0); // Passes });});3. Real Time / Randomness
Problem: Test asserts against Date.now() or Math.random().
Fix: Fake the clock and seed randomness.
// Flakyit('timestamps events', () => { const event = createEvent(); expect(event.createdAt).toBe(Date.now()); // Fails: time moved});
// Fixedit('timestamps events', () => { jest.useFakeTimers(); jest.setSystemTime(new Date('2024-01-15'));
const event = createEvent(); expect(event.createdAt).toEqual(new Date('2024-01-15'));
jest.useRealTimers();});4. Real Network
Problem: Test hits a live API endpoint.
Fix: MSW intercepts everything; an unmocked request is a failure, not a flake to retry.
// Flakyit('fetches user', async () => { // Hits the real API — network failure causes flake const user = await fetch('/api/users/123').then(r => r.json()); expect(user.name).toBe('John');});
// Fixedit('fetches user', async () => { // MSW intercepts and returns mock data server.use( rest.get('/api/users/:id', (req, res, ctx) => res(ctx.json({ id: '123', name: 'John' })) ) );
const user = await fetch('/api/users/123').then(r => r.json()); expect(user.name).toBe('John');});What NOT to Test
Tests cost maintenance; some never repay it. Do not write tests for:
- Implementation details — internal state, private methods, render counts, exact prop values.
- The framework or the library — React, TanStack, Zod, or the standard library are already tested.
- Generated code —
routeTree.gen.ts, codegen output, snapshots of trivial markup. - Pure styling with no behavior — that is Lost Pixel’s job, not an assertion’s.
- Trivial glue — one-line pass-throughs, barrel re-exports, constant maps.
- Third-party network responses literally — mock the contract (MSW), do not hit the live API in CI.
House Rules
- Test behavior, not implementation. Query by role/label/text first (
getByRole,getByLabelText);getByTestIdis the last resort. Never assert on state, props, or internal calls. - Mock at the edge. MSW for the network. Do not mock modules you own — real implementations beat fakes.
- Add a11y and visual checks where they belong. jest-axe in every component test; Lost Pixel on surfaces a diff would catch.
- Flaky tests are defects. Quarantine and fix them. Never retry-til-green, never
test.retry(). - Scope tests to one behavior. One
it()block per behavior; multiple assertions OK if they verify the same behavior.