Code Coverage and CI Integration
Code coverage tells you what percentage of your code runs during tests. High coverage does not guarantee bug-free code, but low coverage often means untested paths that will break in production. In this tutorial, you’ll learn how to measure coverage, configure thresholds, and integrate testing into CI pipelines.
Note: This tutorial builds on concepts covered in End-to-End Testing with Playwright. Make sure you’ve completed that tutorial first if you’re new to testing.
Understanding Code Coverage
Coverage comes in several flavors:
- Line coverage: What percentage of lines executed?
- Branch coverage: What percentage of branches (if/else, ternary) taken?
- Function coverage: What percentage of functions called?
- Statement coverage: What percentage of statements executed?
Jest and Vitest use Istanbul (now Native V8 coverage) by default, providing all these metrics plus more granular information.
Setting Up Coverage
With Jest
Jest has coverage built in. Run tests with the --coverage flag:
npm test -- --coverage
Or configure it in package.json:
{
"jest": {
"collectCoverage": true,
"coverageDirectory": "coverage",
"coverageReporters": ["html", "text", "lcov"]
}
}
With Vitest
Vitest uses V8 coverage by default:
npx vitest run --coverage
Configure in vitest.config.js:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['html', 'text', 'lcov'],
reportsDirectory: './coverage',
},
},
});
Interpreting Coverage Reports
Run a test with coverage to see the output:
// math.js
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
function multiply(a, b) {
return a * b;
}
module.exports = { add, subtract, multiply };
// math.test.js
const { add, subtract, multiply } = require('./math');
test('adds numbers', () => {
expect(add(2, 3)).toBe(5);
});
test('subtracts numbers', () => {
expect(subtract(5, 3)).toBe(2);
});
Run with coverage:
npm test -- --coverage
Output shows:
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
math.js | 75 | 100 | 66 | 75 | 7
----------|---------|----------|---------|---------|-------------------
The multiply function is not tested (line 7), dragging coverage down.
Adding Threshold Enforcement
Prevent coverage from regressing by setting minimum thresholds:
Jest
// jest.config.js
module.exports = {
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
'./src/critical/': {
branches: 90,
functions: 90,
lines: 90,
statements: 90,
},
},
};
If coverage drops below thresholds, Jest exits with failure.
Vitest
// vitest.config.js
export default defineConfig({
test: {
coverage: {
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80,
},
},
},
});
Coverage with TypeScript
Both tools handle TypeScript with proper source mapping:
# Jest with TypeScript
npm install --save-dev @types/jest ts-jest
// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
collectCoverageFrom: [
'src/**/*.{js,ts}',
'!src/**/*.d.ts',
'!src/**/*.test.{js,ts}',
],
};
Ignoring Uncoverage
Sometimes code cannot be covered in tests, and that’s okay. Use pragmas to ignore:
// Using istanbul ignore comments
function /* istanbul ignore next */ legacyCode() {
// This will never run in tests
if (typeof window === 'undefined') {
return 'server';
}
return 'browser';
}
// Ignore specific branches
if (process.env.NODE_ENV === 'development') {
/* istanbul ignore else */
enableDebugLogging();
}
With V8 coverage (Vitest), use c8 comments:
function legacyCode() {
/* c8 ignore next */
return 'unreachable';
}
Use sparingly—frequent ignores may indicate design problems.
Generating HTML Reports
HTML reports let you click through and see exactly which lines are uncovered:
// jest.config.js
module.exports = {
coverageReporters: ['html', 'text-summary'],
coverageDirectory: 'coverage',
};
Open coverage/index.html in a browser to explore.
For Vitest with V8:
export default defineConfig({
test: {
coverage: {
reporter: ['html', 'text-summary'],
reportsDirectory: './coverage',
},
},
});
CI Integration
GitHub Actions
# .github/workflows/test.yml
name: Test and Coverage
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests with coverage
run: npm test
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
GitLab CI
# .gitlab-ci.yml
test:
image: node:20
script:
- npm ci
- npm test
coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/
artifacts:
when: always
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
Netlify / Vercel
Most platforms support coverage reports as build artifacts:
// package.json
{
"scripts": {
"test:ci": "jest --coverage",
"test:ci": "vitest run --coverage"
}
}
Badge Integration
Show coverage status in your README:
Codecov
[](https://codecov.io/gh/username/repo)
Coveralls
[](https://coveralls.io/github/username/repo?branch=main)
Best Practices
-
Start with reasonable thresholds — 80% is a common starting point. Raise over time.
-
Focus on critical code first — Protect business logic and error handling.
-
Don’t chase 100% — Some code (error handlers, legacy branches) cannot reasonably be covered.
-
Review uncovered code — Click through the HTML report to see what’s missing.
-
Use coverage in PRs — Block merges that drop coverage below thresholds.
-
Track trends over time — Coverage should generally increase, not fluctuate wildly.
-
Combine with mutation testing — Coverage measures quantity; mutation testing measures quality.
Coverage vs. Quality
High coverage does not mean good tests. Consider this example:
function divide(a, b) {
if (b === 0) {
return Infinity;
}
return a / b;
}
// Test passes but misses the edge case
test('divides numbers', () => {
expect(divide(10, 2)).toBe(5);
});
This gives 50% branch coverage but misses a critical case. Use coverage as a guide, not a guarantee.
Summary
You now understand how to measure and enforce code coverage:
- Configure Jest or Vitest to collect coverage reports
- Set thresholds to prevent regression
- Interpret the various coverage types
- Use ignore comments sparingly
- Generate HTML reports for exploration
- Integrate coverage checks into CI pipelines
- Display coverage badges in your repository
Coverage is one tool in your testing toolbox. Combined with good test design, code review, and mutation testing, it helps ensure your JavaScript applications are reliable and maintainable.
See Also
testing-e2e-playwright— End-to-end testing with Playwrighttesting-mocking— Mocking modules and dependenciesprocess— Access CI/environment variables