Test Coverage Guidance
[!TIP]
The goal is confidence, not coverage percentage. A 95% coverage score
with tests that only check happy paths is worse than 60% coverage with tests
that catch real regressions.
The Testing Pyramid
/\
/E2E\ Few — slow, brittle, expensive
/------\
/ Integ \ Some — test units working together
/------------\
/ Unit \ Many — fast, isolated, precise
/------------------\
- Unit: one function/class in isolation, dependencies mocked
- Integration: multiple units working together (real DB, real HTTP calls)
- E2E: full user journey through the UI (Playwright, Cypress)
What to Test — Decision Guide
Always write tests for:
- Business logic — calculation, transformation, validation rules
- Edge cases — empty input, null, zero, max values, boundary conditions
- Error paths — what happens when the DB is down, input is invalid, token expires
- Public API contracts — function signatures and return shapes other code depends on
- Security boundaries — auth checks, permission checks, input sanitisation
- Bug fixes — write a test that reproduces the bug before fixing it
Unit test when:
- The logic is pure (same input → same output, no side effects)
- External dependencies can be meaningfully mocked
- Fast feedback during development is important
Integration test when:
- Testing that two systems work together (app + DB, service + cache)
- The interaction between layers matters more than each layer individually
- Mocking would hide the actual failure mode
E2E test when:
- Testing a critical user journey (signup, checkout, login)
- Regression protection on flows that involve multiple pages/services
- Keep E2E tests narrow — only core happy paths
What NOT to Test
- Implementation details (private methods, internal state)
- Third-party library internals
- Trivial getters/setters with no logic
- Configuration files (test they're read correctly, not their values)
Test Structure — Arrange, Act, Assert
describe('calculateDiscount', () => {
it('applies 10% discount for premium users', () => {
// Arrange
const user = { tier: 'premium' };
const price = 100;
// Act
const result = calculateDiscount(user, price);
// Assert
expect(result).toBe(90);
});
it('returns full price for standard users', () => {
const user = { tier: 'standard' };
expect(calculateDiscount(user, 100)).toBe(100);
});
it('throws when price is negative', () => {
expect(() => calculateDiscount({ tier: 'premium' }, -10))
.toThrow('Price cannot be negative');
});
});
Framework Quick Reference
JavaScript / TypeScript
| Framework |
Best for |
| Vitest |
Unit + integration, Vite projects, fast |
| Jest |
Unit + integration, universal |
| Playwright |
E2E, cross-browser, modern |
| Cypress |
E2E, component testing |
| Testing Library |
React/Vue/Angular component tests |
// Vitest / Jest example
import { describe, it, expect, vi } from 'vitest';
it('sends welcome email on user creation', async () => {
const sendEmail = vi.fn();
await createUser({ email: 'a@b.com' }, { sendEmail });
expect(sendEmail).toHaveBeenCalledWith({
to: 'a@b.com',
template: 'welcome',
});
});
Python
# pytest
def test_calculate_discount_for_premium_user():
user = User(tier='premium')
assert calculate_discount(user, price=100) == 90
def test_raises_on_negative_price():
with pytest.raises(ValueError, match='Price cannot be negative'):
calculate_discount(User(tier='premium'), price=-10)
Testing Async Code
// ✅ Await the assertion
it('fetches user by id', async () => {
const user = await getUser(42);
expect(user.name).toBe('Alice');
});
// ✅ Test rejection
it('throws 404 when user not found', async () => {
await expect(getUser(999)).rejects.toThrow('User not found');
});
// ✅ Mock fetch / HTTP calls
vi.mock('./api', () => ({
fetchUser: vi.fn().mockResolvedValue({ id: 1, name: 'Alice' }),
}));
Coverage Targets (Practical)
| Area |
Target |
Rationale |
| Business logic / domain |
90%+ |
High value, high risk |
| API handlers / controllers |
80%+ |
Integration tested |
| UI components |
70%+ |
Testing Library for behaviour |
| Config / boilerplate |
0–40% |
Low ROI |
| Overall |
70–80% |
Above this, diminishing returns |
Coverage % is a floor, not a ceiling. 70% with meaningful tests beats 95% with trivial ones.
CI Integration
Every repo should run tests in CI on every PR:
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: npm test -- --coverage
- run: npm run test:e2e # if applicable
Test Checklist
1---2name: test-coverage-guidance3description: Guides what, when, and how to test code — choosing between unit, integration, and end-to-end tests. Use when writing tests, deciding test strategy, setting up testing frameworks, or when the user asks "what should I test", "how do I test X", "is my test coverage good enough", or "unit vs integration". Applies to any language or framework.4license: Apache-2.05---67# Test Coverage Guidance89> [!TIP]10> The goal is **confidence**, not coverage percentage. A 95% coverage score11> with tests that only check happy paths is worse than 60% coverage with tests12> that catch real regressions.1314---1516## The Testing Pyramid1718```19 /\20 /E2E\ Few — slow, brittle, expensive21 /------\22 / Integ \ Some — test units working together23 /------------\24 / Unit \ Many — fast, isolated, precise25 /------------------\26```2728- **Unit**: one function/class in isolation, dependencies mocked29- **Integration**: multiple units working together (real DB, real HTTP calls)30- **E2E**: full user journey through the UI (Playwright, Cypress)3132---3334## What to Test — Decision Guide3536### Always write tests for:3738- **Business logic** — calculation, transformation, validation rules39- **Edge cases** — empty input, null, zero, max values, boundary conditions40- **Error paths** — what happens when the DB is down, input is invalid, token expires41- **Public API contracts** — function signatures and return shapes other code depends on42- **Security boundaries** — auth checks, permission checks, input sanitisation43- **Bug fixes** — write a test that reproduces the bug before fixing it4445### Unit test when:46- The logic is pure (same input → same output, no side effects)47- External dependencies can be meaningfully mocked48- Fast feedback during development is important4950### Integration test when:51- Testing that two systems work together (app + DB, service + cache)52- The interaction between layers matters more than each layer individually53- Mocking would hide the actual failure mode5455### E2E test when:56- Testing a critical user journey (signup, checkout, login)57- Regression protection on flows that involve multiple pages/services58- Keep E2E tests narrow — only core happy paths5960---6162## What NOT to Test6364- Implementation details (private methods, internal state)65- Third-party library internals66- Trivial getters/setters with no logic67- Configuration files (test they're read correctly, not their values)6869---7071## Test Structure — Arrange, Act, Assert7273```ts74describe('calculateDiscount', () => {75 it('applies 10% discount for premium users', () => {76 // Arrange77 const user = { tier: 'premium' };78 const price = 100;7980 // Act81 const result = calculateDiscount(user, price);8283 // Assert84 expect(result).toBe(90);85 });8687 it('returns full price for standard users', () => {88 const user = { tier: 'standard' };89 expect(calculateDiscount(user, 100)).toBe(100);90 });9192 it('throws when price is negative', () => {93 expect(() => calculateDiscount({ tier: 'premium' }, -10))94 .toThrow('Price cannot be negative');95 });96});97```9899---100101## Framework Quick Reference102103### JavaScript / TypeScript104105| Framework | Best for |106|-----------|----------|107| **Vitest** | Unit + integration, Vite projects, fast |108| **Jest** | Unit + integration, universal |109| **Playwright** | E2E, cross-browser, modern |110| **Cypress** | E2E, component testing |111| **Testing Library** | React/Vue/Angular component tests |112113```ts114// Vitest / Jest example115import { describe, it, expect, vi } from 'vitest';116117it('sends welcome email on user creation', async () => {118 const sendEmail = vi.fn();119 await createUser({ email: 'a@b.com' }, { sendEmail });120 expect(sendEmail).toHaveBeenCalledWith({121 to: 'a@b.com',122 template: 'welcome',123 });124});125```126127### Python128129```python130# pytest131def test_calculate_discount_for_premium_user():132 user = User(tier='premium')133 assert calculate_discount(user, price=100) == 90134135def test_raises_on_negative_price():136 with pytest.raises(ValueError, match='Price cannot be negative'):137 calculate_discount(User(tier='premium'), price=-10)138```139140---141142## Testing Async Code143144```ts145// ✅ Await the assertion146it('fetches user by id', async () => {147 const user = await getUser(42);148 expect(user.name).toBe('Alice');149});150151// ✅ Test rejection152it('throws 404 when user not found', async () => {153 await expect(getUser(999)).rejects.toThrow('User not found');154});155156// ✅ Mock fetch / HTTP calls157vi.mock('./api', () => ({158 fetchUser: vi.fn().mockResolvedValue({ id: 1, name: 'Alice' }),159}));160```161162---163164## Coverage Targets (Practical)165166| Area | Target | Rationale |167|------|--------|-----------|168| Business logic / domain | 90%+ | High value, high risk |169| API handlers / controllers | 80%+ | Integration tested |170| UI components | 70%+ | Testing Library for behaviour |171| Config / boilerplate | 0–40% | Low ROI |172| Overall | 70–80% | Above this, diminishing returns |173174> Coverage % is a floor, not a ceiling. 70% with meaningful tests beats 95% with trivial ones.175176---177178## CI Integration179180Every repo should run tests in CI on every PR:181182```yaml183# .github/workflows/test.yml184name: Tests185on: [push, pull_request]186jobs:187 test:188 runs-on: ubuntu-latest189 steps:190 - uses: actions/checkout@v4191 - uses: actions/setup-node@v4192 with: { node-version: 20 }193 - run: npm ci194 - run: npm test -- --coverage195 - run: npm run test:e2e # if applicable196```197198---199200## Test Checklist201202- [ ] Happy path tested203- [ ] Edge cases tested (empty, null, boundary values)204- [ ] Error/failure paths tested205- [ ] No tests asserting implementation details206- [ ] Tests are deterministic (no random data, no date.now() without mocking)207- [ ] Async code properly awaited208- [ ] CI runs tests on every PR209- [ ] Coverage ≥ 70% on business logic