| name | coverage-analysis |
| description | Analyze test coverage, generate reports, and identify untested code. Use when improving test coverage, ensuring code quality, or preparing for production. |
| allowed-tools | Read, Edit, Bash, Grep, Glob |
Coverage Analysis Skill
This skill helps you analyze and improve test coverage across the monorepo using Vitest's V8 coverage provider.
When to Use This Skill
- Analyzing test coverage across packages
- Identifying untested code paths
- Setting coverage thresholds
- Generating coverage reports
- Improving test quality
- Pre-deployment coverage checks
- Code review coverage validation
Coverage Overview
The project uses Vitest with V8 coverage provider for:
- Line coverage: Percentage of lines executed
- Branch coverage: Percentage of conditional branches tested
- Function coverage: Percentage of functions called
- Statement coverage: Percentage of statements executed
Configuration
Vitest Coverage Config
// vitest.config.ts (root or package-level)
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: true,
environment: "node", // or "jsdom" for frontend
coverage: {
provider: "v8",
reporter: ["text", "json", "html", "lcov"],
reportsDirectory: "./coverage",
exclude: [
"node_modules/",
"__tests__/",
"**/*.test.ts",
"**/*.spec.ts",
"dist/",
"build/",
"*.config.ts",
"*.config.js",
".next/",
".turbo/",
],
include: ["src/**/*.ts", "src/**/*.tsx"],
all: true,
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80,
},
},
},
});
Package-Specific Configs
API Package:
// apps/api/vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
coverage: {
provider: "v8",
reporter: ["text", "json", "html"],
thresholds: {
lines: 85, // Higher threshold for backend
functions: 85,
branches: 80,
statements: 85,
},
exclude: [
"__tests__/",
"src/index.ts", // Exclude entry point
"src/config/**", // Exclude config files
],
},
},
});
Web Package:
// apps/web/vitest.config.ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
test: {
environment: "jsdom",
coverage: {
provider: "v8",
reporter: ["text", "json", "html"],
thresholds: {
lines: 75, // Frontend may have lower threshold
functions: 75,
branches: 70,
statements: 75,
},
exclude: [
"__tests__/",
"src/app/**", // Exclude Next.js app directory
"**/*.config.*",
],
},
},
});
Database Package:
// packages/database/vitest.config.ts
import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
coverage: {
provider: "v8",
reporter: ["text", "json", "html"],
thresholds: {
lines: 90, // Very high threshold for critical package
functions: 90,
branches: 85,
statements: 90,
},
include: ["src/**/*.ts"],
exclude: [
"__tests__/",
"migrations/**", // Exclude migrations
],
},
},
});
Running Coverage
Common Commands
# Generate coverage for all packages
pnpm test:coverage
# Generate coverage for specific package
pnpm -F @sgcarstrends/api test:coverage
pnpm -F @sgcarstrends/web test:coverage
pnpm -F @sgcarstrends/database test:coverage
# Generate coverage with specific reporters
pnpm test:coverage -- --coverage.reporter=html
pnpm test:coverage -- --coverage.reporter=lcov
# Run tests and generate coverage in watch mode
pnpm test:watch -- --coverage
# Generate coverage for changed files only
pnpm test:coverage -- --changed
Package.json Scripts
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:coverage:ui": "vitest --ui --coverage"
}
}
Coverage Reports
Text Report
# Terminal output
pnpm test:coverage
# Example output:
# ----------|---------|----------|---------|---------|-------------------
# File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
# ----------|---------|----------|---------|---------|-------------------
# All files | 87.5 | 83.33 | 85.71 | 87.5 |
# cars.ts | 90 | 85 | 100 | 90 | 45-47
# coe.ts | 85 | 80 | 75 | 85 | 23, 56-58
# ----------|---------|----------|---------|---------|-------------------
HTML Report
# Generate HTML report
pnpm test:coverage
# Open in browser
open coverage/index.html # macOS
xdg-open coverage/index.html # Linux
start coverage/index.html # Windows
# HTML report features:
# - Interactive file browser
# - Line-by-line coverage visualization
# - Color-coded coverage (green = covered, red = uncovered)
# - Branch coverage details
JSON Report
# Generate JSON report
pnpm test:coverage -- --coverage.reporter=json
# Output: coverage/coverage-final.json
{
"path/to/file.ts": {
"lines": { "1": 1, "2": 1, "3": 0 },
"functions": { "functionName": 1 },
"branches": { "0": [1, 0] },
"statements": { "1": 1, "2": 1 }
}
}
LCOV Report
# Generate LCOV format (for CI tools like Codecov, Coveralls)
pnpm test:coverage -- --coverage.reporter=lcov
# Output: coverage/lcov.info
Coverage Thresholds
Setting Thresholds
// vitest.config.ts
export default defineConfig({
test: {
coverage: {
thresholds: {
// Global thresholds
lines: 80,
functions: 80,
branches: 80,
statements: 80,
// Per-file thresholds
perFile: true,
// Fail build if below thresholds
100: false, // Don't require 100% coverage
},
},
},
});
Enforcing Thresholds in CI
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- run: pnpm install
- run: pnpm test:coverage
# Fail if coverage below threshold
- name: Check coverage
run: |
if [ $(jq '.total.lines.pct' coverage/coverage-summary.json | cut -d. -f1) -lt 80 ]; then
echo "Coverage below 80%"
exit 1
fi
Analyzing Coverage
Identify Untested Files
# Generate coverage for all files (including untested)
pnpm test:coverage -- --coverage.all=true
# Find files with 0% coverage
grep -r '"pct": 0' coverage/coverage-final.json
Find Uncovered Lines
# Generate HTML report and inspect
pnpm test:coverage
open coverage/index.html
# Look for red-highlighted lines in HTML report
# These are uncovered lines that need tests
Check Branch Coverage
// Example: Find uncovered branches
function processData(data: any) {
// Branch 1: if condition
if (data.value > 10) {
return "high";
}
// Branch 2: else condition (uncovered)
return "low";
}
// Test both branches
describe("processData", () => {
it("should return high for values > 10", () => {
expect(processData({ value: 15 })).toBe("high");
});
it("should return low for values <= 10", () => {
expect(processData({ value: 5 })).toBe("low");
});
});
Improving Coverage
Strategy 1: Test Untested Functions
// Find untested function
export function calculateCOEPrice(quota: number, bids: number): number {
// Untested
return quota > 0 ? bids / quota : 0;
}
// Add test
describe("calculateCOEPrice", () => {
it("should calculate price when quota is positive", () => {
expect(calculateCOEPrice(100, 50000)).toBe(500);
});
it("should return 0 when quota is 0", () => {
expect(calculateCOEPrice(0, 50000)).toBe(0);
});
});
Strategy 2: Test Error Paths
// Original: Only happy path tested
export async function fetchCarData(month: string) {
const res = await fetch(`/api/cars?month=${month}`);
return res.json(); // What if fetch fails?
}
// Improved: Test error path
describe("fetchCarData", () => {
it("should fetch data successfully", async () => {
// Happy path test
});
it("should handle network errors", async () => {
vi.spyOn(global, "fetch").mockRejectedValue(new Error("Network error"));
await expect(fetchCarData("2024-01")).rejects.toThrow("Network error");
});
it("should handle non-200 responses", async () => {
vi.spyOn(global, "fetch").mockResolvedValue({
ok: false,
status: 500,
} as Response);
await expect(fetchCarData("2024-01")).rejects.toThrow();
});
});
Strategy 3: Test Edge Cases
// Original: Basic test
export function formatMonth(date: Date): string {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}`;
}
// Improved: Test edge cases
describe("formatMonth", () => {
it("should format single-digit months", () => {
expect(formatMonth(new Date("2024-01-01"))).toBe("2024-01");
});
it("should format double-digit months", () => {
expect(formatMonth(new Date("2024-12-01"))).toBe("2024-12");
});
it("should handle leap years", () => {
expect(formatMonth(new Date("2024-02-29"))).toBe("2024-02");
});
});
Strategy 4: Test Conditional Branches
// Function with multiple branches
export function getVehicleCategory(type: string): string {
if (type === "car") return "Category A";
if (type === "motorcycle") return "Category B";
if (type === "taxi") return "Category C";
return "Unknown"; // Often forgotten!
}
// Test all branches
describe("getVehicleCategory", () => {
it.each([
["car", "Category A"],
["motorcycle", "Category B"],
["taxi", "Category C"],
["bus", "Unknown"],
])("should return %s for %s", (type, expected) => {
expect(getVehicleCategory(type)).toBe(expected);
});
});
Coverage in CI/CD
GitHub Actions Integration
# .github/workflows/coverage.yml
name: Coverage
on: [push, pull_request]
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- run: pnpm install
- run: pnpm test:coverage
# Upload coverage to Codecov
- uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
flags: unittests
name: codecov-umbrella
# Upload coverage as artifact
- uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
Coverage Badges
<!-- README.md -->
[](https://codecov.io/gh/username/repo)
Coverage Exclusions
Exclude Specific Code
// Exclude line
/* v8 ignore next */
console.log("Debug statement");
// Exclude block
/* v8 ignore start */
if (process.env.NODE_ENV === "development") {
console.log("Development only");
}
/* v8 ignore stop */
// Exclude function
/* v8 ignore next 5 */
function debugHelper() {
// This entire function is excluded
console.log("Debug");
}
Exclude Files/Directories
// vitest.config.ts
export default defineConfig({
test: {
coverage: {
exclude: [
// Test files
"**/__tests__/**",
"**/*.test.ts",
"**/*.spec.ts",
// Config files
"**/*.config.ts",
"**/*.config.js",
// Build output
"dist/**",
"build/**",
".next/**",
// Specific files
"src/index.ts",
"src/generated/**",
// External dependencies
"node_modules/**",
],
},
},
});
Monorepo Coverage
Aggregate Coverage
# Generate coverage for all packages
pnpm -r test:coverage
# Merge coverage reports (requires custom script)
node scripts/merge-coverage.js
Custom Merge Script
// scripts/merge-coverage.ts
import { readFileSync, writeFileSync } from "fs";
import { glob } from "glob";
const coverageFiles = glob.sync("**/coverage/coverage-final.json", {
ignore: ["node_modules/**"],
});
const merged: any = {};
for (const file of coverageFiles) {
const coverage = JSON.parse(readFileSync(file, "utf-8"));
Object.assign(merged, coverage);
}
writeFileSync("coverage-merged.json", JSON.stringify(merged, null, 2));
console.log("Coverage merged successfully");
Best Practices
1. Set Realistic Thresholds
// ❌ Too strict (100% is often impractical)
thresholds: {
lines: 100,
functions: 100,
branches: 100,
statements: 100,
}
// ✅ Realistic and achievable
thresholds: {
lines: 80,
functions: 80,
branches: 75,
statements: 80,
}
2. Exclude Generated Code
// vitest.config.ts
coverage: {
exclude: [
"src/generated/**",
"*.config.*",
"__tests__/**",
],
}
3. Focus on Critical Paths
// Prioritize testing:
// 1. Business logic
// 2. Data transformations
// 3. API endpoints
// 4. Error handling
// Less critical:
// - UI components (test functionality, not styling)
// - Configuration files
// - Type definitions
4. Track Coverage Over Time
# Store coverage in git (add to .gitignore exceptions)
!coverage/coverage-summary.json
# Track changes
git diff coverage/coverage-summary.json
Troubleshooting
Coverage Not Generated
# Issue: No coverage directory created
# Solution: Ensure tests are running
pnpm test # First run tests
pnpm test:coverage # Then generate coverage
Low Coverage Despite Tests
# Issue: Coverage config excludes tested files
# Solution: Check exclude patterns
# vitest.config.ts
coverage: {
exclude: [
// Remove overly broad patterns
// "src/**", // ❌ This excludes everything!
],
}
Coverage Report Empty
# Issue: Tests passing but coverage 0%
# Solution: Ensure coverage.all is true
coverage: {
all: true, // Include all source files
include: ["src/**/*.ts"],
}
Threshold Failures
# Issue: Coverage below threshold
# Solution: Add missing tests or adjust thresholds
# Lower threshold temporarily
thresholds: {
lines: 70, // Reduced from 80
}
# Or add tests to increase coverage
References
- Vitest Coverage: https://vitest.dev/guide/coverage
- V8 Coverage: https://v8.dev/blog/javascript-code-coverage
- Istanbul (alternative): https://istanbul.js.org
- Related files:
vitest.config.ts- Coverage configuration- Root CLAUDE.md - Testing guidelines
Best Practices Summary
- Set Realistic Thresholds: 80% is good, 100% is often impractical
- Exclude Non-Critical Code: Config files, generated code, tests
- Focus on Critical Paths: Business logic, APIs, error handling
- Test All Branches: Ensure conditional logic is tested
- Track Over Time: Monitor coverage trends
- Use HTML Reports: Visualize uncovered lines
- Integrate with CI: Enforce thresholds in pipelines
- Don't Game Coverage: Write meaningful tests, not just for coverage