Testing in Practice · Testing APIs, Contracts and User Journeys

Coverage, Test Speed and Tests in CI

Measure branch coverage to find untested logic, set sensible thresholds, and organise fast and slow test stages in CI.

Create a free account to save this page, take notes and track your progress. Sign in

What you'll build

A coverage report with branch coverage for a pricing module, showing exactly which conditions no test exercises, a CI threshold that fails the build when coverage drops, and a CI layout that runs fast tests first.

The idea

  • Line coverage shows which lines ran; branch coverage also checks both outcomes of each condition. Use branch coverage.
  • Coverage finds untested code; it doesn't prove tests are good (a test with no assertions still "covers" code).
  • Set a threshold (--cov-fail-under) to stop coverage eroding; ratchet it up, don't chase 100%.
  • In CI: lint and unit tests first (seconds), then integration tests with real services, then a small end-to-end suite; run tests in parallel when suites grow.

The code

Branch coverage with pytest-cov
# file: pricing.py
def final_price(cents: int, member: bool, coupon: str | None = None) -> int:
    if member:
        cents = int(cents * 0.9)
    if coupon == "FREESHIP":
        cents -= 0                      # shipping handled elsewhere
    elif coupon == "TENOFF":
        cents = max(0, cents - 1000)
    if cents > 100_000:
        raise ValueError("needs manual approval")
    return cents

# file: test_pricing.py
from pricing import final_price

def test_members_get_ten_percent_off():
    assert final_price(2000, member=True) == 1800

def test_ten_off_coupon():
    assert final_price(2000, member=False, coupon="TENOFF") == 1000

# file: main.py
import pytest
pytest.main(["-q", "-p", "no:cacheprovider", "--cov=pricing", "--cov-branch", "--cov-report=term-missing",
             "--cov-fail-under=90"])

Output

..
ERROR: Coverage failure: total of 78 is less than fail-under=90
                                                                         [100%]
================================ tests coverage ================================
_______________ coverage: platform linux, python 3.12.3-final-0 ________________

Name         Stmts   Miss Branch BrPart  Cover   Missing
--------------------------------------------------------
pricing.py      10      2      8      2    78%   5, 9
--------------------------------------------------------
TOTAL           10      2      8      2    78%
FAIL Required test coverage of 90% not reached. Total coverage: 77.78%
2 passed in <time>

Run this on your computer — see “Get Started” for setup.

The report lists lines 5 and 9 as never run (the free-shipping coupon and the manual-approval limit) and two partially covered branches. The build fails because coverage is below 90%. A CI pipeline might look like this:

# .github/workflows/tests.yml
name: tests
on: [push, pull_request]
jobs:
  unit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-python@v6
        with: { python-version: "3.14" }
      - run: pip install -r requirements-dev.txt
      - run: pytest -m "not integration and not e2e" --cov --cov-branch --cov-fail-under=85 -n auto
  integration:
    needs: unit
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:18
        env: { POSTGRES_PASSWORD: test }
        ports: ["5432:5432"]
    steps:
      - uses: actions/checkout@v5
      - uses: actions/setup-python@v6
        with: { python-version: "3.14" }
      - run: pip install -r requirements-dev.txt
      - run: pytest -m integration

Common mistakes

  • Treating a coverage percentage as a quality target and writing assertion-free tests to hit it.
  • Line coverage only, missing untested else paths.
  • One giant CI job where a slow end-to-end suite delays feedback on simple unit failures.
  • Not tracking test duration; a suite that grows from 2 to 20 minutes changes how people work.

Key points

  • Use branch coverage to find untested logic.
  • A ratcheting threshold prevents erosion; 100% isn't the goal.
  • Run fast tests first and parallelise as suites grow.

Further reading: pytest-cov · coverage.py: branch coverage · GitHub Actions: service containers

Practice questions

What does branch coverage measure that line coverage doesn't?

Answer

Whether both outcomes of each decision were exercised, so an if without an else path tested shows up as partially covered.

Why isn't 100% coverage a good goal on its own?

Answer

Coverage only shows code ran, not that tests check the right outcomes; chasing 100% produces low-value tests.

How should a CI pipeline order test stages?

Answer

Fast checks first (lint, unit tests), then integration tests with real services, then a small end-to-end suite, so feedback is quick.

Open in the app to mark lessons complete, take notes and track your progress.

Was this page helpful?