Code Coverage and CI Integration

· 5 min read · Updated March 16, 2026 · intermediate
coverage ci jest vitest github-actions gitlab-ci

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

[![Codecov](https://codecov.io/gh/username/repo/branch/main/graph/badge.svg)](https://codecov.io/gh/username/repo)

Coveralls

[![Coveralls](https://coveralls.io/repos/github/username/repo/badge.svg?branch=main)](https://coveralls.io/github/username/repo?branch=main)

Best Practices

  1. Start with reasonable thresholds — 80% is a common starting point. Raise over time.

  2. Focus on critical code first — Protect business logic and error handling.

  3. Don’t chase 100% — Some code (error handlers, legacy branches) cannot reasonably be covered.

  4. Review uncovered code — Click through the HTML report to see what’s missing.

  5. Use coverage in PRs — Block merges that drop coverage below thresholds.

  6. Track trends over time — Coverage should generally increase, not fluctuate wildly.

  7. 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