Skip to main content

Test Coverage

Test Coverage measures the percentage of your code that is executed by automated tests. It helps identify untested code paths and assess the completeness of your test suite.


What is Test Coverage?

Test coverage is a metric that shows how much of your codebase is tested:

Test Coverage = (Lines Executed by Tests / Total Lines) × 100%

Example

// src/utils/discount.ts
export function calculateDiscount(price: number, percent: number): number {
if (price < 0) {
throw new Error('Price cannot be negative');
}

if (percent < 0 || percent > 100) {
throw new Error('Percent must be between 0 and 100');
}

return price * (percent / 100);
}

Test with 100% coverage:

// tests/unit/utils/discount.test.ts
describe('calculateDiscount()', () => {
test('should calculate discount correctly', () => {
expect(calculateDiscount(100, 10)).toBe(10);
});

test('should throw error for negative price', () => {
expect(() => calculateDiscount(-100, 10)).toThrow('Price cannot be negative');
});

test('should throw error for invalid percent', () => {
expect(() => calculateDiscount(100, 150)).toThrow('Percent must be between 0 and 100');
});
});

Coverage report:

File: discount.ts
--------------------|---------|----------|---------|---------|
| % Stmts | % Branch | % Funcs | % Lines |
--------------------|---------|----------|---------|---------|
discount.ts | 100 | 100 | 100 | 100 |
--------------------|---------|----------|---------|---------|

✅ All lines executed
✅ All branches tested (if/else paths)
✅ All functions tested

Types of Coverage

1. Line Coverage

Percentage of code lines executed:

function example(x: number) {
let result = 0; // Line 1
if (x > 0) { // Line 2
result = x * 2; // Line 3
}
return result; // Line 4
}

// Test: example(5)
// Lines executed: 1, 2, 3, 4 = 100% line coverage

2. Branch Coverage

Percentage of if/else paths tested:

function validate(age: number) {
if (age >= 18) { // Branch 1: true path
return 'Adult';
} else { // Branch 2: false path
return 'Minor';
}
}

// Test 1: validate(20) → Covers true branch
// Test 2: validate(15) → Covers false branch
// Total: 100% branch coverage (both paths tested)

Why it matters:

function processPayment(amount: number) {
if (amount > 1000) {
return applyDiscountRules(amount); // Complex logic here!
}
return amount;
}

// ❌ Only testing amount < 1000 → 50% branch coverage
// ✅ Test both branches → 100% branch coverage

3. Function Coverage

Percentage of functions called:

// file.ts
export function used() { /* ... */ }
export function unused() { /* ... */ } // Never called in tests!

// Function coverage: 50% (1 of 2 functions tested)

4. Statement Coverage

Percentage of statements executed (most granular):

function calculate(x: number, y: number) {
const sum = x + y; // Statement 1
const product = x * y; // Statement 2
const average = sum / 2; // Statement 3
return average; // Statement 4
}

// Test: calculate(5, 10)
// All 4 statements executed = 100% statement coverage

Coverage Report Example

npm run test:coverage

--------------------|---------|----------|---------|---------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
--------------------|---------|----------|---------|---------|-------------------|
All files | 85.2 | 82.4 | 90.1 | 85.0 | |
src/ | 88.5 | 85.6 | 92.3 | 88.2 | |
auth.ts | 94.3 | 91.2 | 100.0 | 94.1 | 45-47,92 |
order.ts | 82.1 | 79.8 | 84.6 | 81.9 | 23-25,67-70 |
src/utils/ | 91.2 | 88.9 | 95.0 | 91.0 | |
discount.ts | 100.0 | 100.0 | 100.0 | 100.0 | |
format.ts | 82.4 | 77.8 | 90.0 | 82.1 | 12-15 |
--------------------|---------|----------|---------|---------|-------------------|

✅ Target met: 85.2% (target: 85%)
⚠️ Uncovered lines: 12 lines need tests

Visual Coverage Report (HTML):

npm run test:coverage -- --coverage-html

# Opens coverage/lcov-report/index.html
# Interactive HTML showing:
# - Green = Covered lines
# - Red = Uncovered lines
# - Yellow = Partially covered (branches)

Test Coverage in SpecWeave

SpecWeave tracks coverage per task in tasks.md (v0.7.0+):

Example Task with Coverage

## T-001: Implement Authentication Service

**AC**: AC-US1-01, AC-US1-02

**Test Plan** (BDD format):
- **Given** valid credentials → **When** login → **Then** return JWT token
- **Given** invalid password → **When** login → **Then** throw error

**Test Cases**:
- Unit (`auth.test.ts`):
- validLogin, invalidPassword, nonexistentUser, rateLimiting
- **Coverage**: 90% (target: 85-90%)

- Integration (`auth-flow.test.ts`):
- loginEndpoint, sessionPersistence, databaseIntegration
- **Coverage**: 85% (target: 80-85%)

**Overall Coverage**: 87% ✅ (target: 85%)

**Uncovered Lines**: 45-47 (error logging), 92 (admin bypass - tested manually)

**Implementation**: AuthService.ts, password hashing, JWT generation

Coverage Targets by Test Type

Test TypeSpecWeave TargetRationale
Unit Tests85-90%Core business logic thoroughly tested
Integration Tests80-85%Integration points verified
E2E Tests100% critical pathsKey user flows work end-to-end
Overall85%+High confidence, reasonable effort

Coverage Visualization


The 100% Coverage Myth

Question: Should we aim for 100% test coverage?

Answer: No. 100% coverage has diminishing returns.

Why Not 100%?

1. Cost vs Benefit

// Critical business logic (MUST be tested)
export function calculateRefund(order: Order): number {
if (order.status === 'cancelled' && order.isPaid) {
const fee = order.total * 0.05; // 5% cancellation fee
return order.total - fee;
}
return 0;
}

// Test: 100% coverage = 4 tests (all branches) → HIGH VALUE

// Trivial getter (low testing value)
export class User {
get email(): string {
return this._email;
}
}

// Test: 100% coverage = 1 test (getter returns value) → LOW VALUE
// Better: Integration test that uses email naturally

2. Uncoverable Code

Some code paths are hard to test:

export function saveFile(path: string, data: string): void {
try {
fs.writeFileSync(path, data);
} catch (error) {
// Coverage: Hard to trigger filesystem errors in unit tests
if (error.code === 'ENOSPC') {
throw new Error('Disk full');
} else if (error.code === 'EACCES') {
throw new Error('Permission denied');
}
throw error;
}
}

// Better: Test via integration test with mock filesystem
// Or: Accept 85% coverage, test common paths only

3. False Sense of Security

// ❌ 100% coverage, but WRONG test
function multiply(a: number, b: number): number {
return a + b; // BUG: Should be a * b
}

test('should multiply numbers', () => {
const result = multiply(2, 3);
expect(result).toBe(5); // Test passes, but logic is wrong!
});

// Coverage: 100% ✅
// Correctness: 0% ❌

Coverage ≠ Correctness


Best Practices

1. Focus on Critical Paths

// ✅ HIGH PRIORITY: Payment processing (test thoroughly)
test('should process payment and update order status', async () => {
// 90%+ coverage for payment logic
});

// 🟡 MEDIUM PRIORITY: UI formatting (test key cases)
test('should format currency correctly', () => {
// 80% coverage for formatters
});

// 🟢 LOW PRIORITY: Constants and getters (test via integration)
export const CONFIG = {
apiUrl: 'https://api.example.com'
};
// No dedicated test needed (used in integration tests)

2. Test Behavior, Not Coverage

// ❌ Bad: Writing tests just to increase coverage
test('should have name property', () => {
const user = new User('John', 'john@example.com');
expect(user.name).toBe('John'); // Trivial test
});

// ✅ Good: Test meaningful behavior
test('should prevent duplicate email registration', async () => {
await userService.register('john@example.com', 'password123');

await expect(
userService.register('john@example.com', 'other password')
).rejects.toThrow('Email already exists');
});

3. Use Coverage to Find Gaps

# Generate coverage report
npm run test:coverage

# Review uncovered lines
# Ask: "Are these critical paths?"
# If yes → Add tests
# If no → Document why (comments in code)

Example:

export function processOrder(order: Order): void {
// ... main processing logic (tested: 95% coverage)

if (process.env.NODE_ENV === 'development') {
// Debug logging (uncovered, documented)
// COVERAGE: Not tested (dev-only code path)
console.log('Order processed:', order.id);
}
}

4. Set Realistic Targets

// jest.config.js
module.exports = {
coverageThreshold: {
global: {
statements: 85, // SpecWeave target
branches: 80,
functions: 85,
lines: 85
},
// Stricter for critical code
'./src/services/payment/': {
statements: 95,
branches: 90,
functions: 95,
lines: 95
},
// Relaxed for utilities
'./src/utils/': {
statements: 75,
branches: 70,
functions: 75,
lines: 75
}
}
};

Coverage Anti-Patterns

1. Testing for Coverage Numbers

// ❌ Bad: Test just to hit coverage target
test('should set email', () => {
const user = new User();
user.email = 'test@example.com';
expect(user.email).toBe('test@example.com');
});

// ✅ Good: Test real behavior
test('should validate email format', () => {
const user = new User();
expect(() => {
user.email = 'invalid-email';
}).toThrow('Invalid email format');
});

2. Ignoring Important Edge Cases

// ❌ Bad: High coverage, but missing edge cases
function divide(a: number, b: number): number {
return a / b;
}

test('should divide numbers', () => {
expect(divide(10, 2)).toBe(5); // 100% coverage, but...
});

// ✅ Good: Test edge cases
test('should handle division by zero', () => {
expect(() => divide(10, 0)).toThrow('Cannot divide by zero');
});

test('should handle negative numbers', () => {
expect(divide(-10, 2)).toBe(-5);
});

3. Mocking Too Much

// ❌ Bad: Mocked so heavily, test is meaningless
test('should save user', async () => {
const mockDb = {
save: jest.fn().mockResolvedValue({ id: 123 })
};
const service = new UserService(mockDb);

const user = await service.create({ email: 'test@example.com' });

expect(mockDb.save).toHaveBeenCalled(); // 100% coverage, but...
// We didn't test if data is correct, validation works, etc.
});

// ✅ Good: Use real database (integration test)
test('should save user to database', async () => {
const service = new UserService(testDb);
const user = await service.create({ email: 'test@example.com' });

// Verify in real database
const saved = await testDb.users.findById(user.id);
expect(saved.email).toBe('test@example.com');
});

Tools for Measuring Coverage

Jest (JavaScript/TypeScript)

# Generate coverage report
npm test -- --coverage

# Coverage in watch mode
npm test -- --coverage --watch

# Specific threshold
npm test -- --coverage --coverageThreshold='{"global":{"lines":85}}'

Istanbul/nyc (JavaScript)

# Install
npm install --save-dev nyc

# Run with coverage
nyc npm test

# HTML report
nyc --reporter=html npm test

Coverage.py (Python)

# Install
pip install coverage

# Run with coverage
coverage run -m pytest

# Report
coverage report
coverage html

SpecWeave Coverage Validation

SpecWeave provides /specweave:validate-coverage command (v0.7.0+):

# Validate coverage for increment
/specweave:validate-coverage 0008

# Output:
✅ Increment 0008: User Authentication
--------------------|---------|----------|---------|---------|
Task | % Stmts | % Branch | % Funcs | % Lines | Target |
--------------------|---------|----------|---------|---------|--------|
T-001: AuthService | 90.1 | 88.5 | 95.0 | 90.0 | 85-90% |
T-002: Login API | 85.3 | 82.1 | 90.0 | 85.1 | 80-85% |
T-003: Login UI | 87.2 | 85.9 | 92.0 | 87.0 | 85-90% |
--------------------|---------|----------|---------|---------|--------|
Overall | 87.5 | 85.5 | 92.3 | 87.4 | 85%+ |

🎉 All coverage targets met!


Summary

Test coverage measures code tested by automated tests:

  • Line coverage: % of lines executed
  • Branch coverage: % of if/else paths tested
  • Function coverage: % of functions called
  • Statement coverage: % of statements executed

SpecWeave coverage targets:

  • Unit tests: 85-90% coverage (core logic)
  • Integration tests: 80-85% coverage (integration points)
  • E2E tests: 100% critical paths (key user flows)
  • Overall: 85%+ (high confidence)

Key insight: Coverage is a guide, not a goal. 85% coverage with meaningful tests beats 100% coverage with trivial tests. Focus on critical paths and behavior, not numbers.