coverage-strategies.md 5.5 KB

Coverage Strategies

Comprehensive code coverage with pytest-cov.

Setup

pip install pytest-cov

Basic Usage

# Run with coverage
pytest --cov=src

# With terminal report
pytest --cov=src --cov-report=term

# With HTML report
pytest --cov=src --cov-report=html
open htmlcov/index.html

# Multiple formats
pytest --cov=src --cov-report=term --cov-report=html --cov-report=xml

Coverage Configuration

pyproject.toml

[tool.coverage.run]
source = ["src"]
branch = true
omit = [
    "*/tests/*",
    "*/__init__.py",
    "*/migrations/*",
]

[tool.coverage.report]
exclude_lines = [
    "pragma: no cover",
    "def __repr__",
    "raise NotImplementedError",
    "if TYPE_CHECKING:",
    "if __name__ == .__main__.:",
]
fail_under = 80
show_missing = true

[tool.coverage.html]
directory = "htmlcov"

.coveragerc (Alternative)

[run]
source = src
branch = true
omit =
    */tests/*
    */__init__.py

[report]
exclude_lines =
    pragma: no cover
    raise NotImplementedError
fail_under = 80

[html]
directory = htmlcov

Branch Coverage

# branch=true catches this
def process(value):
    if value > 0:
        return "positive"
    # Missing else branch without branch coverage
    return "non-positive"

# Test both branches
def test_positive():
    assert process(5) == "positive"

def test_non_positive():
    assert process(-1) == "non-positive"

Excluding Code

def debug_only():  # pragma: no cover
    """Never executed in production."""
    print("Debug info")

if TYPE_CHECKING:  # Excluded by default config
    from typing import Optional

def platform_specific():
    if sys.platform == "win32":  # pragma: no cover
        return windows_implementation()
    return unix_implementation()

Coverage in CI

GitHub Actions

name: Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Install dependencies
        run: pip install -e .[test]

      - name: Run tests with coverage
        run: pytest --cov=src --cov-report=xml

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v4
        with:
          files: ./coverage.xml
          fail_ci_if_error: true

Fail on Low Coverage

# Fail if coverage below 80%
pytest --cov=src --cov-fail-under=80

Measuring Coverage of Specific Tests

# Coverage for specific test file
pytest tests/test_api.py --cov=src/api

# Coverage for marked tests only
pytest -m "unit" --cov=src

# Coverage for specific module
pytest --cov=src/module_name

Combining Coverage

# Run tests in parallel, combine coverage
pytest -n auto --cov=src --cov-append

# Or manually combine
coverage combine
coverage report

Coverage Diff (Incremental)

# Show coverage for changed lines only (with diff-cover)
pip install diff-cover

pytest --cov=src --cov-report=xml
diff-cover coverage.xml --compare-branch=origin/main

Mutation Testing

# Beyond coverage: test quality with mutmut
pip install mutmut

# Run mutation testing
mutmut run --paths-to-mutate=src/

# View results
mutmut results
mutmut html

Coverage Reports

Terminal Report

pytest --cov=src --cov-report=term-missing

Output:

Name                      Stmts   Miss Branch BrPart  Cover   Missing
---------------------------------------------------------------------
src/api.py                   50      5     12      2    88%   45-49, 67
src/utils.py                 30      0      8      0   100%
---------------------------------------------------------------------
TOTAL                        80      5     20      2    92%

HTML Report

pytest --cov=src --cov-report=html
# Creates htmlcov/index.html with line-by-line highlighting

XML Report (CI)

pytest --cov=src --cov-report=xml
# Creates coverage.xml for CI tools

JSON Report

pytest --cov=src --cov-report=json
# Creates coverage.json for programmatic access

Coverage Best Practices

1. Aim for Meaningful Coverage

# BAD: 100% coverage but no assertions
def test_function():
    result = my_function()  # Just call it

# GOOD: Meaningful assertions
def test_function():
    result = my_function()
    assert result.status == "success"
    assert len(result.items) > 0

2. Don't Chase 100%

# Some code genuinely shouldn't be tested
def __repr__(self):  # pragma: no cover
    return f"<User {self.name}>"

if __name__ == "__main__":  # pragma: no cover
    main()

3. Focus on Critical Paths

# Prioritize coverage for:
# - Business logic
# - Error handling
# - Edge cases
# - Security-sensitive code

4. Use Branch Coverage

[tool.coverage.run]
branch = true

5. Track Coverage Trends

# In CI: fail on coverage decrease
- name: Check coverage
  run: |
    pytest --cov=src --cov-report=xml
    diff-cover coverage.xml --compare-branch=origin/main --fail-under=90

Quick Reference

Command Description
--cov=src Enable coverage for src/
--cov-report=term Terminal report
--cov-report=html HTML report
--cov-report=xml XML report (CI)
--cov-fail-under=80 Fail if under 80%
--cov-branch Enable branch coverage
--cov-append Append to existing data
--no-cov Disable coverage