Test Coverage Analysis Skill
When analyzing test coverage, follow this structured process. The goal is not 100% coverage — it's ensuring the riskiest code is tested well.
1. Discover the Testing Setup
Before analyzing, understand the project's testing landscape:
bash
# Detect testing framework # Node.js cat package.json | grep -E "jest|vitest|mocha|ava|tap|playwright|cypress|testing-library" # Python cat requirements.txt pyproject.toml setup.cfg 2>/dev/null | grep -E "pytest|unittest|nose|coverage|tox" # Ruby cat Gemfile 2>/dev/null | grep -E "rspec|minitest|capybara|factory_bot" # Go grep -r "_test.go" --include="*.go" -l . # Java cat pom.xml build.gradle 2>/dev/null | grep -E "junit|mockito|testng|jacoco" # PHP cat composer.json 2>/dev/null | grep -E "phpunit|pest|mockery"
Identify:
- •Testing framework in use (Jest, Vitest, Pytest, RSpec, JUnit, etc.)
- •Coverage tool configured (Istanbul/nyc, coverage.py, SimpleCov, JaCoCo, etc.)
- •Test directory structure (co-located vs separate test folder)
- •Naming conventions (*.test.ts, .spec.ts, test_.py, *_test.go)
- •Test types present (unit, integration, e2e, snapshot)
- •CI integration (are tests running in CI? is coverage enforced?)
2. Run Existing Coverage
bash
# Node.js (Jest) npx jest --coverage --coverageReporters=text # Node.js (Vitest) npx vitest run --coverage # Python (Pytest) python -m pytest --cov=. --cov-report=term-missing # Go go test -coverprofile=coverage.out ./... go tool cover -func=coverage.out # Ruby (RSpec) COVERAGE=true bundle exec rspec # Java (Maven + JaCoCo) mvn test jacoco:report # PHP (PHPUnit) php artisan test --coverage
Record:
- •Overall line coverage percentage
- •Overall branch coverage percentage
- •Files with 0% coverage (completely untested)
- •Files with < 50% coverage (poorly tested)
- •Uncovered lines (specific line numbers)
3. Identify What's NOT Tested
3a. Find Files Without Tests
bash
# Node.js — find source files without matching test files
find src -name "*.ts" -o -name "*.js" | while read f; do
base=$(basename "$f" | sed 's/\.\(ts\|js\)$//')
if ! find . -name "${base}.test.*" -o -name "${base}.spec.*" | grep -q .; then
echo "NO TEST: $f"
fi
done
# Python — find modules without test files
find src -name "*.py" ! -name "__init__.py" | while read f; do
base=$(basename "$f" .py)
if ! find . -name "test_${base}.py" -o -name "${base}_test.py" | grep -q .; then
echo "NO TEST: $f"
fi
done
# Go — find packages without test files
find . -name "*.go" ! -name "*_test.go" -exec dirname {} \; | sort -u | while read d; do
if ! ls "$d"/*_test.go 2>/dev/null | grep -q .; then
echo "NO TEST: $d"
fi
done
3b. Find Untested Code Paths
Look for these commonly missed patterns:
- •Error handlers and catch blocks — the most commonly untested code
- •Edge cases — null, undefined, empty arrays, zero, negative numbers, boundary values
- •Else branches — the unhappy path in if/else
- •Switch default cases — fallback handling
- •Early returns and guard clauses — validation at the top of functions
- •Timeout and retry logic — what happens when things fail
- •Race conditions — concurrent operations
- •Cleanup code — finally blocks, destructors, shutdown handlers
- •Configuration branches — code that runs differently per environment
- •Deprecated or feature-flagged code — code behind flags that's still reachable
3c. Find Dead or Unreachable Code
bash
# Node.js — find unused exports
npx ts-prune
# Python — find unused code
pip install vulture && vulture src/
# General — find functions not referenced anywhere
grep -rn "function\|def\|func " src/ | while read line; do
fname=$(echo "$line" | grep -oP '(?:function|def|func)\s+\K\w+')
count=$(grep -rn "$fname" src/ | wc -l)
if [ "$count" -le 1 ]; then
echo "POSSIBLY UNUSED: $line"
fi
done
4. Risk-Based Prioritization
Not all untested code is equally important. Prioritize by risk:
🔴 Critical — Test These First
- •Authentication and authorization — login, signup, password reset, permission checks
- •Payment and billing — charge, refund, subscription logic
- •Data mutation — create, update, delete operations
- •API endpoints — especially public-facing ones
- •Input validation — sanitization and parsing of user input
- •Security-sensitive code — encryption, token generation, access control
- •Core business logic — the main value of your application
🟠 High — Test These Next
- •Error handling — catch blocks, error boundaries, fallback behavior
- •Database queries — complex queries, transactions, migrations
- •Third-party integrations — API calls, webhooks, callbacks
- •State management — reducers, stores, state transitions
- •File operations — uploads, downloads, processing
- •Background jobs — queues, cron jobs, workers
🟡 Medium — Test When Possible
- •UI components — interactive components, forms, modals
- •Utility functions — helpers, formatters, transformers
- •Configuration — environment-specific logic
- •Middleware — request/response processing pipeline
- •Caching logic — cache invalidation, TTL, fallbacks
🟢 Low — Test If Time Permits
- •Static components — presentational components with no logic
- •Type definitions — interfaces, types, enums
- •Constants and config objects — static values
- •Logging — log formatting and output
- •Dev-only code — seeders, fixtures, debug utilities
5. Test Quality Analysis
Coverage percentage alone doesn't mean tests are good. Analyze quality:
Assertion Quality
code
// 🔴 BAD — test runs but asserts nothing meaningful
test('creates user', async () => {
const result = await createUser({ name: 'Alice' });
expect(result).toBeTruthy(); // too vague
});
// ✅ GOOD — specific, meaningful assertions
test('creates user with correct fields', async () => {
const result = await createUser({ name: 'Alice' });
expect(result.id).toBeDefined();
expect(result.name).toBe('Alice');
expect(result.createdAt).toBeInstanceOf(Date);
});
Test Independence
code
// 🔴 BAD — tests depend on each other's state
let userId;
test('creates user', async () => {
const user = await createUser({ name: 'Alice' });
userId = user.id;
});
test('fetches user', async () => {
const user = await getUser(userId); // depends on previous test
});
// ✅ GOOD — each test sets up its own state
test('fetches user', async () => {
const created = await createUser({ name: 'Alice' });
const fetched = await getUser(created.id);
expect(fetched.name).toBe('Alice');
});
Common Test Smells
- •No assertions — test runs code but checks nothing
- •Testing implementation, not behavior — brittle tests that break on refactors
- •Over-mocking — mocking so much that the test proves nothing
- •Flaky tests — tests that pass/fail randomly (timing, order-dependent, network)
- •Duplicate tests — same scenario tested multiple times in different places
- •Giant test files — 1000+ line test files that are hard to maintain
- •Missing cleanup — tests that leave behind state (DB records, files, env vars)
- •Snapshot overuse — snapshots accepted without review, hiding regressions
- •Copy-paste tests — duplicated setup that should be extracted into helpers
- •Happy path only — only testing success, never failure
6. Stack-Specific Checks
Node.js / Jest / Vitest
- •Check for missing
afterEachcleanup (open handles, DB connections) - •Verify async tests use
awaitor return promises (silent failures otherwise) - •Check for missing
jest.mock()cleanup between tests - •Look for
setTimeoutin tests withoutjest.useFakeTimers() - •Verify snapshot tests are intentional and reviewed
- •Check for missing error boundary tests in React components
- •Verify
act()wrapping on React state updates in tests - •Look for missing
waitFor/findByon async UI updates
Python / Pytest
- •Check for missing
conftest.pyfixtures for common setup - •Verify database tests use transactions and rollback (
@pytest.mark.django_db) - •Look for missing
parametrizeon tests that should cover multiple inputs - •Check for missing
mock.patchcleanup (use context managers or decorators) - •Verify async tests use
@pytest.mark.asyncio - •Check for missing exception tests (
with pytest.raises(...)) - •Look for hardcoded file paths in tests (use
tmp_pathfixture) - •Verify test isolation — no tests reading/writing shared state
React / Next.js
- •Check for missing
rendertests on all user-facing components - •Verify form components test validation, submission, and error states
- •Look for missing accessibility tests (
@testing-library/jest-dommatchers) - •Check for untested loading and error states
- •Verify hooks are tested with
renderHookfrom testing-library - •Check for missing tests on context providers and consumers
- •Look for untested route guards and redirects
- •Verify Server Components have integration tests
- •Check for missing tests on API routes / Server Actions
Vue / Nuxt
- •Check for missing
mount/shallowMounttests on components - •Verify Pinia stores are tested with
createTestingPinia - •Look for missing
emitted()checks on event emissions - •Check for untested computed properties and watchers
- •Verify composables are tested independently
- •Check for missing tests on Nuxt middleware and plugins
Go
- •Check for missing table-driven tests on functions with multiple inputs
- •Verify error return values are tested (not just happy path)
- •Look for missing
t.Parallel()on independent tests - •Check for missing
t.Helper()on test utility functions - •Verify interfaces are tested with mock implementations
- •Check for missing benchmark tests on performance-critical code (
func BenchmarkX) - •Look for missing
t.Cleanup()for resource teardown - •Verify HTTP handlers are tested with
httptest.NewServer
Java / Spring Boot
- •Check for missing
@SpringBootTestintegration tests - •Verify
@MockBeanis used appropriately (not over-mocked) - •Look for missing
@Transactionalon database tests (auto rollback) - •Check for missing controller tests with
MockMvc - •Verify exception handlers are tested
- •Check for missing
@ParameterizedTeston multi-input tests - •Look for untested
@Scheduledtasks and async methods - •Verify repository custom queries have integration tests
Ruby / RSpec
- •Check for missing
describeblocks for each public method - •Verify
letandbeforeblocks handle proper setup/teardown - •Look for missing
contextblocks for different scenarios - •Check for missing
shared_examplesfor common behavior - •Verify factory definitions cover all required fields (
FactoryBot) - •Check for missing request specs on API endpoints
- •Look for untested ActiveRecord callbacks and validations
- •Verify background jobs (Sidekiq/Resque) have specs
PHP / Laravel
- •Check for missing Feature tests on routes and controllers
- •Verify database tests use
RefreshDatabaseorDatabaseTransactions - •Look for missing tests on form requests (validation rules)
- •Check for missing
assertDatabaseHas/assertDatabaseMissingassertions - •Verify mail, notification, and event fakes are used
- •Check for missing tests on Eloquent scopes and accessors
- •Look for untested middleware
- •Verify queue jobs and listeners have tests
Mobile (React Native / Flutter)
- •Check for missing widget/component tests
- •Verify navigation flows are tested
- •Look for missing tests on platform-specific code (iOS vs Android)
- •Check for untested offline/error states
- •Verify async storage operations are tested
- •Check for missing tests on deep link handling
- •Look for untested permission request flows
- •Verify API response parsing is tested with realistic mock data
API / Integration Tests
- •Check for missing tests on all HTTP methods (GET, POST, PUT, DELETE, PATCH)
- •Verify authentication is tested (valid token, expired token, no token, wrong role)
- •Look for missing tests on pagination, filtering, and sorting
- •Check for missing tests on rate limiting behavior
- •Verify webhook handlers are tested with realistic payloads
- •Check for missing tests on file upload/download endpoints
- •Look for untested CORS behavior
- •Verify error responses match API documentation/contract
Database
- •Check for missing migration tests (up and rollback)
- •Verify complex queries are tested with realistic data volumes
- •Look for missing tests on database constraints (unique, foreign key, check)
- •Check for untested transaction behavior (commit, rollback, deadlock)
- •Verify connection pooling and timeout handling is tested
- •Check for missing seed data validation tests
7. Coverage Improvement Plan
After analysis, provide a prioritized plan:
Immediate (This Sprint)
- •List specific files and functions to test first based on risk
- •Provide test scaffolding for the highest-priority untested code
- •Suggest specific test cases with descriptions
Short-Term (Next 2 Sprints)
- •Increase coverage on medium-risk areas
- •Add integration tests for critical user flows
- •Fix test quality issues (weak assertions, flaky tests)
Long-Term (This Quarter)
- •Set up coverage thresholds in CI (fail build if coverage drops)
- •Add e2e tests for critical user journeys
- •Implement mutation testing to verify test effectiveness
- •Set up coverage trend tracking
Output Format
Coverage Report
Overall Coverage:
| Metric | Current | Target | Gap |
|---|---|---|---|
| Line Coverage | X% | 80% | X% |
| Branch Coverage | X% | 70% | X% |
| Function Coverage | X% | 85% | X% |
Untested Files (by risk):
🔴 Critical — No Tests:
- •
src/auth/login.ts— handles user authentication - •
src/payments/charge.ts— processes payments
🟠 High — Partial Coverage:
- •
src/api/users.ts— 40% covered, missing error paths - •
src/db/queries.ts— 55% covered, missing edge cases
Test Quality Issues:
- •
tests/user.test.ts:45— assertion too vague (toBeTruthy) - •
tests/order.test.ts— tests are order-dependent - •
tests/api.test.ts:120— over-mocked, not testing real behavior
For Each Untested Area
File: src/auth/login.ts
- •Risk: 🔴 Critical
- •Why it matters: Handles user authentication — bugs here = security breach
- •Missing tests:
- •Valid login with correct credentials
- •Login with wrong password (should return 401)
- •Login with non-existent email (should return 401, same message as wrong password)
- •Login with expired account
- •Rate limiting after 5 failed attempts
- •SQL injection attempt in email field
- •Session creation and token generation
- •Test scaffold:
code
describe('login', () => {
it('returns a token for valid credentials', async () => { ... });
it('returns 401 for incorrect password', async () => { ... });
it('returns 401 for non-existent email', async () => { ... });
it('locks account after 5 failed attempts', async () => { ... });
it('rejects SQL injection in email', async () => { ... });
});
Summary
End every analysis with:
- •Current state — Overall coverage and quality assessment
- •Biggest gaps — The riskiest untested code
- •Top 5 tests to write first — Highest impact, with brief descriptions
- •Test quality issues — Problems with existing tests that need fixing
- •CI recommendations — Coverage thresholds, pre-commit hooks, reporting
- •What's done well — Good testing practices already in place to maintain