Codecov Tutorial: Track Test Coverage in CI/CD

Codecov Tutorial: Track Test Coverage in CI/CD

Codecov is a coverage tracking service that aggregates coverage reports from your CI runs, displays trends over time, and adds pull request comments showing coverage changes for every commit. This tutorial covers setup, configuration, PR integration, and coverage gates.

Why Codecov Over Just Viewing Coverage Locally

Running nyc report or pytest --cov locally tells you current coverage, but it doesn't:

  • Track whether coverage is trending up or down over time
  • Block merges when coverage drops below a threshold
  • Show reviewers which new code is uncovered in a PR
  • Aggregate coverage across parallel CI jobs

Codecov handles all of this with a few lines of CI configuration.

Supported Languages and Frameworks

Codecov accepts any lcov.info, coverage.xml, or coverage.json file. Common integrations:

Language Coverage tool Report format
JavaScript/TypeScript Istanbul (nyc), c8, Vitest lcov.info
Python pytest-cov, coverage.py coverage.xml
Go go test -coverprofile coverage.out
Ruby SimpleCov coverage.xml
Java JaCoCo, Cobertura .xml
PHP phpunit clover.xml
Rust cargo-tarpaulin lcov.info

Setting Up Codecov

1. Connect Your Repository

  1. Go to codecov.io and sign in with GitHub, GitLab, or Bitbucket
  2. Select your organization and repository
  3. Copy the CODECOV_TOKEN from the repository settings

Add the token to your CI secrets:

  • GitHub Actions: Settings → Secrets and variables → New repository secret
  • GitLab: Settings → CI/CD → Variables
  • CircleCI: Project Settings → Environment Variables

Public repositories on GitHub don't need a token — Codecov can read public repos without authentication.

2. Generate a Coverage Report

Your test run must produce a coverage file. Examples:

JavaScript with nyc:

npx nyc --reporter=lcov mocha tests/**/*.test.js
# Produces: coverage/lcov.info

JavaScript with Vitest:

// vitest.config.js
export default {
  test: {
    coverage: {
      provider: 'v8',
      reporter: ['text', 'lcov'],
    }
  }
};
npx vitest run --coverage
# Produces: coverage/lcov.info

Python with pytest:

pip install pytest-cov
pytest --cov=src --cov-report=xml tests/
# Produces: coverage.xml

Go:

go test -coverprofile=coverage.out ./...
<span class="hljs-comment"># Produces: coverage.out

3. Upload to Codecov in CI

GitHub Actions

name: CI

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      
      - run: npm ci
      
      - name: Run tests with coverage
        run: npx nyc --reporter=lcov mocha tests/**/*.test.js
      
      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          files: ./coverage/lcov.info
          flags: unit
          name: unit-tests
          fail_ci_if_error: true

GitLab CI

test:
  image: node:20
  script:
    - npm ci
    - npx nyc --reporter=lcov mocha tests/**/*.test.js
  after_script:
    - curl -Os https://uploader.codecov.io/latest/linux/codecov
    - chmod +x codecov
    - ./codecov -t $CODECOV_TOKEN -f coverage/lcov.info
  coverage: '/Lines\s*:\s*(\d+\.?\d*)/'

CircleCI

version: 2.1

jobs:
  test:
    docker:
      - image: cimg/node:20.0
    steps:
      - checkout
      - run: npm ci
      - run: npx nyc --reporter=lcov mocha tests/**/*.test.js
      - run:
          name: Upload coverage
          command: bash <(curl -s https://codecov.io/bash) -t $CODECOV_TOKEN

The Codecov YAML Configuration

Place codecov.yml at your repository root to customize behavior:

coverage:
  # Target coverage for the whole repo
  status:
    project:
      default:
        target: 80%          # What we aim for
        threshold: 1%        # Allow 1% drop before failing
        
    # Per-PR patch coverage (new code only)
    patch:
      default:
        target: 75%          # New code should be 75%+ covered
        threshold: 0%        # No tolerance for patch drops
  
  # Round coverage percentages
  precision: 2
  range: "70...100"         # Color range for UI (red=70, green=100)

# Ignore these paths in coverage calculations
ignore:
  - "tests/**"
  - "**/*.test.js"
  - "**/*.spec.js"
  - "src/generated/**"
  - "src/migrations/**"
  - "scripts/**"

# Comment behavior on PRs
comment:
  layout: "reach,diff,flags,files,footer"
  behavior: default          # Post once, update on each push
  require_changes: false     # Always comment, even if no change
  require_base: true         # Don't comment if base report missing

PR Coverage Comments

When Codecov sees a pull request, it posts a comment like:

## Coverage Report

Coverage diff: **+2.31%** ✅

| Flag | Coverage Δ |
|------|-----------|
| unit | 87.45% (+2.31%) ✅ |

| File | Coverage Δ |
|------|-----------|
| src/auth/login.js | 92.00% (+15.00%) ✅ |
| src/utils/format.js | 76.00% (-4.00%) ❌ |

The patch coverage section shows coverage specifically for the lines changed in this PR — critical for catching untested new code before merge.

Flags for Multiple Coverage Reports

When you have separate unit and integration test suites, upload each separately with flags:

# GitHub Actions
- name: Upload unit coverage
  uses: codecov/codecov-action@v4
  with:
    token: ${{ secrets.CODECOV_TOKEN }}
    files: ./coverage/unit/lcov.info
    flags: unit

- name: Upload integration coverage  
  uses: codecov/codecov-action@v4
  with:
    token: ${{ secrets.CODECOV_TOKEN }}
    files: ./coverage/integration/lcov.info
    flags: integration

In codecov.yml, configure flags:

flags:
  unit:
    carryforward: true       # Use last run if this CI job didn't run
    paths:
      - src/
  integration:
    carryforward: true
    paths:
      - src/

Codecov merges the flag reports for the combined view. This is especially useful for monorepos.

Monorepo Setup

For monorepos with multiple packages:

# codecov.yml
codecov:
  notify:
    after_n_builds: 3       # Wait for all 3 packages to upload

flags:
  packages/api:
    paths:
      - packages/api/src/
  packages/ui:
    paths:
      - packages/ui/src/
  packages/shared:
    paths:
      - packages/shared/src/

Upload from each package job:

- name: Upload API coverage
  uses: codecov/codecov-action@v4
  with:
    token: ${{ secrets.CODECOV_TOKEN }}
    files: packages/api/coverage/lcov.info
    flags: packages/api

Coverage Gates: Blocking Merges

To prevent coverage from dropping, configure branch protection:

  1. In your repository settings, add Codecov as a required status check
  2. Set fail_ci_if_error: true in the GitHub Action
  3. Configure thresholds in codecov.yml

When a PR would drop project coverage below threshold, the Codecov check fails and blocks merge.

Carryforward Coverage

When you have multiple CI jobs and not all run on every commit (e.g., integration tests only on main), use carryforward:

flags:
  integration:
    carryforward: true

Codecov uses the last known coverage for that flag if no new upload arrives for the current commit. This prevents false "coverage dropped" alerts when the integration test job didn't run.

The Codecov Badge

Add a coverage badge to your README:

[![codecov](https://codecov.io/gh/your-org/your-repo/branch/main/graph/badge.svg?token=YOUR_TOKEN)](https://codecov.io/gh/your-org/your-repo)

The badge updates automatically after each CI run.

Codecov vs Coveralls vs SonarQube

Feature Codecov Coveralls SonarQube
PR comments
Trend charts
Code quality
Self-hosted Enterprise ✓ (open source)
Free tier Public repos Public repos Community edition
Monorepo support Good Basic Good

Codecov is the most popular choice for JavaScript/Python open source projects. SonarQube adds code quality analysis (duplication, complexity, security issues) beyond just coverage — worth evaluating if you want a single tool for both.

Continuous Production Monitoring Beyond Coverage

Codecov tracks which code is covered by tests. But coverage tells you nothing about whether the covered code actually works correctly in production.

HelpMeTest monitors your production application 24/7, running end-to-end tests every 5 minutes and alerting you when real user flows break. Where Codecov shows you coverage trends, HelpMeTest shows you whether production is actually working.

Start with the free plan — 10 tests, unlimited health checks, no credit card required. It takes about 10 minutes to have your first production monitor running.

Summary

Setting up Codecov:

  1. Sign up at codecov.io, connect your repo, get the token
  2. Generate coverage in your test run (lcov.info, coverage.xml, etc.)
  3. Add the Codecov upload step to your CI pipeline
  4. Create codecov.yml with project targets and patch thresholds
  5. Enable branch protection to block PRs that drop coverage
  6. Add the badge to your README

The PR comment integration is the highest-value feature — reviewers see exactly which new lines are uncovered before approving a merge, making coverage a team concern rather than an afterthought.

Read more