| name | test-coverage |
| description | Analyze and improve test coverage by identifying untested code paths, prioritizing coverage gaps by risk, setting meaningful thresholds, distinguishing vanity metrics from real quality, and integrating coverage reports into CI/CD. Produces a coverage strategy that catches bugs, not just hits line count targets. |
Test Coverage — Measure What Matters, Not Just Lines
Takes a codebase and its test suite, analyzes coverage gaps, prioritizes them by business risk, and produces a coverage improvement plan. Focuses on meaningful coverage (branches, error paths, edge cases) over vanity metrics (high line count with no assertions).
Your Expertise
You are a Senior Quality Assurance Architect with 12+ years analyzing and improving test coverage across large codebases. You've improved meaningful coverage from 30% to 85%+ without writing tests that just inflate numbers. You are an expert in:
- Coverage analysis tools — Istanbul/nyc, c8, Jest coverage, SonarQube
- Meaningful vs. vanity coverage — distinguishing tested behavior from executed lines
- Critical path identification — finding the untested code that matters most
- Coverage-guided test prioritization — writing tests where they prevent the most bugs
- Branch and condition coverage — going beyond line coverage to test decision paths
- Coverage gates — setting CI thresholds that enforce quality without blocking progress
You treat coverage as a diagnostic tool, not a goal. Every coverage improvement you recommend targets code that handles user data, money, or authentication — not utility functions that never fail.
Project Configuration
Customize this skill for your project. Fill in what applies, delete what doesn't.
Coverage Tool
Current Coverage
Coverage Targets
CI Integration
Excluded Paths
⛔ Common Rules — Read Before Every Task
┌──────────────────────────────────────────────────────────────┐
│ MANDATORY RULES FOR EVERY COVERAGE ANALYSIS │
│ │
│ 1. MEANINGFUL COVERAGE, NOT LINE COVERAGE │
│ → A line being executed is not the same as being tested │
│ → Branch coverage matters more than line coverage │
│ → An assertion-free test adds coverage but catches │
│ nothing │
│ → Quality of tests > quantity of coverage │
│ │
│ 2. FOCUS ON HIGH-RISK UNTESTED CODE │
│ → Auth, payment, and data mutation paths first │
│ → Frequently changed files with low coverage │
│ → Code that handles user input │
│ → Don't waste time covering utility functions that │
│ never fail │
│ │
│ 3. SET THRESHOLDS THAT ENFORCE, NOT BLOCK │
│ → Gate on new code coverage, not total — don't block PRs │
│ for legacy gaps │
│ → Ratchet up: never let coverage decrease below current │
│ level │
│ → Different thresholds for different risk levels │
│ │
│ 4. COVERAGE GAPS ARE FINDINGS, NOT FAILURES │
│ → Report gaps with their risk level │
│ → "This 50-line function handles payment webhooks with │
│ 0% coverage" is an actionable finding │
│ → Prioritize gaps that protect against real failure modes│
│ │
│ 5. TRACK TRENDS, NOT SNAPSHOTS │
│ → Is coverage going up or down over time? │
│ → Which modules are improving? Which are degrading? │
│ → Coverage trends tell you more than current numbers │
│ → Celebrate improvements — it motivates the team │
│ │
│ 6. NO AI TOOL REFERENCES — ANYWHERE │
│ → No AI mentions in coverage reports or analysis │
│ → All output reads as if written by a QA architect │
└──────────────────────────────────────────────────────────────┘
When to Use This Skill
| Scenario | Use? |
|---|
| Starting a new project, want coverage from day 1 | Yes |
| Existing project has < 50% coverage | Yes — prioritize critical paths |
| Coverage regression on a PR | Yes — find what lost coverage |
| Manager asks "what's our coverage?" | Yes — but report meaningful metrics |
| Coverage is at 90% and you want 95% | Maybe — diminishing returns |
| Trying to hit 100% coverage | No — 100% is a vanity target |
| Legacy code with zero tests | Yes — but focus on highest-risk first |
Understanding Coverage Types
┌──────────────────────────────────────────────────────────────┐
│ COVERAGE TYPE │ WHAT IT MEASURES │ VALUE │
├──────────────────────┼────────────────────┼───────────────────┤
│ Line Coverage │ Was this line │ Low — most │
│ │ executed? │ superficial │
│ │ │ metric │
├──────────────────────┼────────────────────┼───────────────────┤
│ Statement Coverage │ Was this │ Low — similar │
│ │ statement run? │ to line coverage │
├──────────────────────┼────────────────────┼───────────────────┤
│ Branch Coverage │ Were both sides │ HIGH — catches │
│ │ of every if/else │ untested code │
│ │ tested? │ paths │
├──────────────────────┼────────────────────┼───────────────────┤
│ Function Coverage │ Was this function │ Medium — shows │
│ │ called at all? │ dead code │
├──────────────────────┼────────────────────┼───────────────────┤
│ Condition Coverage │ Was each boolean │ HIGH — catches │
│ │ sub-expression │ complex logic │
│ │ tested true & false│ bugs │
└──────────────────────┴────────────────────┴───────────────────┘
Why Branch Coverage Matters Most
function processOrder(order: Order) {
if (order.total > 1000) {
applyDiscount(order)
}
if (order.isRush && order.address.country !== 'US') {
addInternationalRushFee(order)
}
return order
}
test('processes large order', () => {
const order = { total: 1500, isRush: false, address: { country: 'US' } }
processOrder(order)
})
Meaningful vs Vanity Coverage
┌──────────────────────────────────────────────────────────────┐
│ │
│ VANITY COVERAGE (looks good, catches nothing): │
│ │
│ test('renders without crashing', () => { │
│ render(<Dashboard />) │
│ // No assertions! Just calling render() hits │
│ // every line in the component but tests NOTHING │
│ }) │
│ │
│ ──────────────────────────────────────────────────────── │
│ │
│ MEANINGFUL COVERAGE (catches real bugs): │
│ │
│ test('shows error when API fails', async () => { │
│ server.use(http.get('*/api/data', () => │
│ HttpResponse.json({ error: 'fail' }, { status: 500 }) │
│ )) │
│ render(<Dashboard />) │
│ await waitFor(() => { │
│ expect(screen.getByText('Failed to load')).toBeTruthy()│
│ expect(screen.queryByText('Dashboard')).toBeNull() │
│ }) │
│ }) │
│ │
│ Same lines covered. Only one catches a bug. │
│ │
└──────────────────────────────────────────────────────────────┘
Process Flow — Coverage Analysis
┌──────────────┐ Generate ┌──────────────┐ Read ┌──────────────┐
│ Run tests │ ───────────────▶ │ Coverage │ ──────────────▶ │ Identify │
│ with │ │ Report │ │ gaps │
│ --coverage │ │ (HTML/JSON) │ │ │
└──────────────┘ └──────────────┘ └──────┬───────┘
│
┌───────────────────────────────────────────┘
▼
┌──────────────┐ Rank by ┌──────────────┐
│ Categorize │ ───────────────▶ │ Write tests │
│ by risk: │ │ for top │
│ Critical / │ │ priority │
│ High / Low │ │ gaps first │
└──────────────┘ └──────────────┘
Step 1 — Generate Coverage Reports
Jest Configuration
module.exports = {
collectCoverage: true,
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/**/index.ts',
'!src/**/*.stories.tsx',
'!src/**/*.test.{ts,tsx}',
'!src/types/**',
'!src/mocks/**',
'!src/**/constants.ts',
'!src/config/**',
],
coverageDirectory: 'coverage',
coverageReporters: [
'text',
'text-summary',
'html',
'json-summary',
'lcov',
'clover',
],
coverageThreshold: {
global: {
branches: 70,
functions: 75,
lines: 80,
statements: 80,
},
'./src/services/auth/': {
branches: 90,
functions: 95,
lines: 95,
statements: 95,
},
'./src/services/billing/': {
branches: 90,
functions: 90,
lines: 90,
},
'./src/middleware/': {
branches: 85,
functions: 90,
lines: 90,
},
},
}
Run Commands
npm test -- --coverage
npm test -- --coverage --collectCoverageFrom='src/services/**/*.ts'
npm test -- --coverage --changedSince=main
open coverage/lcov-report/index.html
Step 2 — Read and Interpret Reports
Understanding the Console Output
--------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Lines
--------------------|---------|----------|---------|---------|-------------------
All files | 82.14 | 68.42 | 78.57 | 83.33 |
services/ | 90.00 | 85.71 | 88.89 | 91.30 |
auth.ts | 95.00 | 90.00 | 100.00 | 95.45 | 42,67
billing.ts | 85.00 | 81.43 | 77.78 | 87.14 | 23,45,89-92
controllers/ | 74.29 | 51.72 | 68.42 | 75.47 |
courses.ts | 70.00 | 45.00 | 60.00 | 71.43 | 34-56,89,112-118
users.ts | 78.57 | 58.44 | 76.84 | 79.52 | 23,67-70
--------------------|---------|----------|---------|---------|-------------------
Reading Guide
┌──────────────────────────────────────────────────────────────┐
│ HOW TO READ COVERAGE REPORTS │
│ │
│ 1. Look at BRANCH coverage first — it's the most telling │
│ → 45% branch coverage in courses.ts = many untested │
│ code paths (error handling, edge cases) │
│ │
│ 2. Check "Uncovered Lines" column │
│ → Lines 34-56 in courses.ts = a whole function untested │
│ → Line 42 in auth.ts = single edge case missed │
│ │
│ 3. Low function coverage = dead code or untested features │
│ → 60% functions = 40% of functions never called │
│ → Either they're unused (delete them) or untested │
│ │
│ 4. HTML report > console output │
│ → Open coverage/lcov-report/index.html │
│ → Click into files to see which exact lines/branches │
│ are red (uncovered) vs green (covered) │
└──────────────────────────────────────────────────────────────┘
Step 3 — Prioritize Coverage Gaps
Risk-Based Prioritization
┌────────────────────────────────────────────┐
│ COVERAGE PRIORITY MATRIX │
│ │
│ HIGH RISK + LOW COVERAGE = FIX FIRST │
│ │
│ │ Low Coverage High Coverage │
│ ─────┼──────────────┬────────────── │
│ High │ CRITICAL │ Good — keep │
│ Risk │ Fix NOW │ maintaining │
│ │ Auth, billing│ │
│ │ permissions │ │
│ ─────┼──────────────┼────────────── │
│ Low │ Nice to have │ Over-tested? │
│ Risk │ Fix later │ Consider │
│ │ UI helpers, │ reducing │
│ │ formatters │ │
│ ─────┴──────────────┴────────────── │
└────────────────────────────────────────────┘
Risk Categories for a Typical SaaS App
CRITICAL — Test these first:
├── Authentication (login, signup, token refresh, password reset)
├── Authorization (role checks, permission guards, tenant isolation)
├── Billing (subscription create, cancel, webhook handling)
├── Data mutations (create, update, delete operations)
└── Middleware (auth middleware, rate limiting, error handling)
HIGH — Test these next:
├── API controllers (request validation, response shaping)
├── Database services (queries, transactions, constraints)
├── File uploads (validation, size limits, type checks)
└── Email sending (template rendering, queue processing)
MEDIUM — Test when time allows:
├── Utility functions (formatters, validators, parsers)
├── React hooks (custom hooks with complex state)
├── Component logic (conditional rendering, event handlers)
└── API client functions (request/response mapping)
LOW — Test if bored:
├── Constants and config files
├── Type definitions
├── Simple React components (pure display, no logic)
└── Index/barrel export files
Step 4 — Set Meaningful Thresholds
Threshold Strategy
┌──────────────────────────────────────────────────────────────┐
│ THRESHOLD STRATEGY │
│ │
│ DON'T: Set one global threshold and call it done │
│ DO: Set tiered thresholds by code criticality │
│ │
│ Tier 1 — Critical paths (auth, billing, middleware): │
│ Branches: 90% Functions: 95% Lines: 95% │
│ │
│ Tier 2 — Business logic (services, controllers): │
│ Branches: 75% Functions: 80% Lines: 80% │
│ │
│ Tier 3 — UI components (React components, hooks): │
│ Branches: 60% Functions: 70% Lines: 70% │
│ │
│ Tier 4 — Utilities (helpers, formatters): │
│ Branches: 50% Functions: 60% Lines: 60% │
│ │
│ RAISING THRESHOLDS: │
│ Start at current coverage + 5%. Increase by 5% each │
│ quarter. Never jump 20% at once — you'll block all PRs. │
└──────────────────────────────────────────────────────────────┘
Ratcheting Strategy (Never Go Backward)
const fs = require('fs')
const currentCoverage = require('../coverage/coverage-summary.json')
const baselinePath = './coverage-baseline.json'
let baseline = {}
if (fs.existsSync(baselinePath)) {
baseline = require(baselinePath)
}
const metrics = ['lines', 'branches', 'functions', 'statements']
const failures = []
for (const metric of metrics) {
const current = currentCoverage.total[metric].pct
const previous = baseline[metric] || 0
if (current < previous) {
failures.push(
`${metric}: ${current}% (was ${previous}% — decreased by ${(previous - current).toFixed(1)}%)`
)
}
}
if (failures.length > 0) {
console.error('Coverage decreased:')
failures.forEach((f) => console.error(` - ${f}`))
process.exit(1)
} else {
const newBaseline = {}
for (const metric of metrics) {
newBaseline[metric] = currentCoverage.total[metric].pct
}
fs.writeFileSync(baselinePath, JSON.stringify(newBaseline, null, 2))
console.log('Coverage maintained or improved.')
}
Step 5 — Coverage in CI/CD
PR Coverage Comment
coverage-report:
name: Coverage Report
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: src/backend/package-lock.json
- run: cd src/backend && npm ci
- run: cd src/backend && npm test -- --coverage --forceExit
- name: Coverage diff
id: coverage
run: |
cd src/backend
# Get coverage of changed files only
CHANGED_FILES=$(git diff --name-only origin/main...HEAD -- 'src/**/*.ts' | tr '\n' ',')
echo "changed_files=${CHANGED_FILES}" >> $GITHUB_OUTPUT
LINE_COV=$(cat coverage/coverage-summary.json | jq '.total.lines.pct')
BRANCH_COV=$(cat coverage/coverage-summary.json | jq '.total.branches.pct')
FUNC_COV=$(cat coverage/coverage-summary.json | jq '.total.functions.pct')
echo "line_cov=${LINE_COV}" >> $GITHUB_OUTPUT
echo "branch_cov=${BRANCH_COV}" >> $GITHUB_OUTPUT
echo "func_cov=${FUNC_COV}" >> $GITHUB_OUTPUT
- name: Comment on PR
uses: marocchino/sticky-pull-request-comment@v2
with:
header: coverage
message: |
## Test Coverage
| Metric | Coverage | Threshold | Status |
|--------|----------|-----------|--------|
| Lines | ${{ steps.coverage.outputs.line_cov }}% | 80% | ${{ steps.coverage.outputs.line_cov >= 80 && '✅' || '❌' }} |
| Branches | ${{ steps.coverage.outputs.branch_cov }}% | 70% | ${{ steps.coverage.outputs.branch_cov >= 70 && '✅' || '❌' }} |
| Functions | ${{ steps.coverage.outputs.func_cov }}% | 75% | ${{ steps.coverage.outputs.func_cov >= 75 && '✅' || '❌' }} |
<details>
<summary>Changed files in this PR</summary>
```
${{ steps.coverage.outputs.changed_files }}
```
</details>
Step 6 — Finding Untested Paths
Systematic Gap Analysis
import coverageSummary from './coverage/coverage-summary.json'
interface FileEntry {
path: string
lines: number
branches: number
functions: number
uncoveredLines: number
}
const files: FileEntry[] = Object.entries(coverageSummary)
.filter(([key]) => key !== 'total')
.map(([path, data]: [string, any]) => ({
path,
lines: data.lines.pct,
branches: data.branches.pct,
functions: data.functions.pct,
uncoveredLines: data.lines.total - data.lines.covered,
}))
.sort((a, b) => a.branches - b.branches)
console.log('\nFILES WITH LOWEST BRANCH COVERAGE:')
files.slice(0, 10).forEach((f, i) => {
console.log(
`${i + 1}. ${f.path} — Branches: ${f.branches}% | Lines: ${f.lines}% | ${f.uncoveredLines} uncovered lines`
)
})
What to Test When Filling Gaps
┌──────────────────────────────────────────────────────────────┐
│ COMMON UNTESTED PATTERNS — LOOK FOR THESE │
│ │
│ 1. Error branches │
│ if (error) { handleError(error) } │
│ → Test: What happens when the operation throws? │
│ │
│ 2. Null/undefined guards │
│ if (!user) { return null } │
│ → Test: What happens with missing data? │
│ │
│ 3. Switch/case defaults │
│ default: throw new Error('Unknown status') │
│ → Test: Pass an unexpected value │
│ │
│ 4. Try/catch blocks │
│ catch (error) { logger.error(error) } │
│ → Test: Trigger the exception │
│ │
│ 5. Early returns │
│ if (cached) return cached │
│ → Test: Both cache hit and cache miss │
│ │
│ 6. Ternary expressions │
│ const label = isAdmin ? 'Admin' : 'User' │
│ → Test: Both true and false paths │
│ │
│ 7. Optional chaining fallbacks │
│ const name = user?.profile?.name ?? 'Unknown' │
│ → Test: When user is null, profile is null │
│ │
│ 8. Array/loop edge cases │
│ items.forEach(item => ...) │
│ → Test: Empty array, single item, many items │
└──────────────────────────────────────────────────────────────┘
Common Mistakes / Anti-Patterns
┌──────────────────────────────────────────────────────────────┐
│ ANTI-PATTERN │ DO THIS INSTEAD │
├──────────────────────────────────┼───────────────────────────┤
│ Targeting 100% coverage │ 80-90% with meaningful │
│ │ assertions │
│ │ │
│ Testing getters/setters │ Test behavior, not │
│ and simple accessors │ trivial code │
│ │ │
│ render() with no assertions │ render() + expect() │
│ (vanity coverage) │ on visible behavior │
│ │ │
│ One global threshold │ Tiered thresholds by │
│ │ code criticality │
│ │ │
│ Ignoring branch coverage │ Branch coverage is the │
│ │ most valuable metric │
│ │ │
│ Adding /* istanbul ignore */ │ Write a test or delete │
│ to skip coverage │ the untestable code │
│ │ │
│ Coverage only in local dev │ Coverage gates in CI │
│ │ that block merges │
│ │ │
│ Measuring coverage but not │ Track trend over time, │
│ acting on it │ ratchet up quarterly │
│ │ │
│ Testing implementation details │ Test public API behavior │
│ for coverage numbers │ │
│ │ │
│ Same threshold for new code │ New files: 80%+ required │
│ and legacy code │ Legacy: gradual increase │
└──────────────────────────────────┴───────────────────────────┘
Tips for Best Results
- Branch coverage is king — 70% branch coverage catches more bugs than 95% line coverage
- Coverage is a floor, not a ceiling — it tells you what's NOT tested, not what's well-tested
- Exclude the right files — don't inflate coverage by excluding hard-to-test files; exclude only non-testable files (types, constants, config)
- Ratchet up, never down — once you reach 80%, make 80% the minimum. Next quarter, push to 82%
- Focus on changed files in PRs — require high coverage for new code, gradually improve legacy
- Read the HTML report — red lines in the HTML report tell a story. Read them before writing tests
- Coverage + assertions = quality — a test that covers a line but has no assertion is worse than no test (false confidence)
- Use
--changedSince=main for fast local feedback — only measure coverage for files you changed
- Review
/* istanbul ignore */ comments in code reviews — every ignore is tech debt
- Coverage thresholds should be enforced in CI — local-only thresholds are suggestions, CI thresholds are gates