Claude
Skills
Sign in
Back

pytest-coverage-measurement

Included with Lifetime
$97 forever

Measure and track test coverage: coverage thresholds by layer, coverage reporting (HTML/terminal), identifying coverage gaps, branch coverage vs line coverage, coverage-driven testing. Includes layer-specific targets (domain 95%, application 85%, adapters 75%). Use when: Measuring test coverage, setting coverage thresholds, identifying untested code paths, tracking coverage improvements, ensuring code quality gates.

Web Dev

What this skill does


# Pytest Coverage Measurement

## Purpose

Code coverage measures how much of your code is tested. This skill provides strategies for meaningful coverage measurement and improving test quality.


## When to Use This Skill

Use when measuring test coverage with "measure coverage", "track coverage", "identify untested code", or "set coverage thresholds".

Do NOT use for writing tests (use layer-specific testing skills), pytest configuration (use `pytest-configuration`), or fixing low coverage (identify gaps first, then use appropriate testing skill).
## Quick Start

Generate coverage report:

```bash
# Generate HTML coverage report
pytest --cov=app --cov-report=html --cov-report=term-missing

# View HTML report
open htmlcov/index.html

# Fail if coverage below threshold
pytest --cov=app --cov-fail-under=80
```

## Instructions

### Step 1: Configure Coverage in pyproject.toml

```toml
[tool.pytest.ini_options]
addopts = [
    "--cov=app",                        # Source to measure
    "--cov-report=html",                # HTML report
    "--cov-report=term-missing",        # Terminal with missing lines
    "--cov-fail-under=80",              # Fail if < 80%
]

[tool.coverage.run]
source = ["app"]
branch = true  # Measure branch coverage (if/else paths)

omit = [
    "*/tests/*",
    "*/__pycache__/*",
    "*/venv/*",
    "*/.venv/*",
]

[tool.coverage.report]
precision = 2
show_missing = true
skip_covered = false  # Show all files, including 100% covered

# Lines to exclude from coverage
exclude_lines = [
    "pragma: no cover",                 # Manual exclusion
    "def __repr__",                     # Repr methods
    "raise NotImplementedError",        # Abstract methods
    "if TYPE_CHECKING:",                # Type checking only
    "if __name__ == .__main__.:",       # CLI entry points
    "@(abc\\.)?abstractmethod",         # Abstract methods
    "class .*\\bProtocol\\):",          # Protocols
]

[tool.coverage.html]
directory = "htmlcov"  # Output directory
```

### Step 2: Understand Line vs Branch Coverage

```python
from __future__ import annotations


# Line coverage: counts executed lines
# Branch coverage: counts each if/else path

def validate_order(order: Order) -> bool:
    """Example of branch coverage."""
    if not order.line_items:  # Branch 1: True
        return False  # Branch 2: False (2 paths)

    if order.total_price < 0:  # Branch 3: True
        return False  # Branch 4: False (2 more paths)

    return True  # Branch 5: Total 4 unique paths


# Test 1: Only tests the happy path
def test_valid_order():
    order = Order(line_items=[item], total_price=Money(100))
    assert validate_order(order) is True
    # Coverage: 5 lines, 2 branches (50% branch coverage)


# Test 2-5: Cover all paths for 100% branch coverage
def test_empty_items():
    order = Order(line_items=[], total_price=Money(100))
    assert validate_order(order) is False

def test_negative_total():
    order = Order(line_items=[item], total_price=Money(-100))
    assert validate_order(order) is False

def test_valid_order_all_paths():
    order = Order(line_items=[item], total_price=Money(100))
    assert validate_order(order) is True
    # Coverage: 5 lines, 4 branches (100% branch coverage)
```

### Step 3: Set Coverage Targets by Layer

```python
# Domain Layer: 95-100% coverage
# app/extraction/domain/
# app/storage/domain/
# app/reporting/domain/
#
# Pure business logic, no dependencies → easy to test exhaustively


# Application Layer: 85-95% coverage
# app/extraction/application/
# app/storage/application/
# app/reporting/application/
#
# Use cases, orchestration → test main paths, some error paths


# Adapter Layer: 75-85% coverage
# app/extraction/adapters/
# app/storage/adapters/
# app/reporting/adapters/
#
# External integrations → test critical paths, less error paths


# Infrastructure Layer: 60-75% coverage
# app/shared/
# Configuration, setup code → test critical paths only
```

### Step 4: Run Coverage and Analyze Report

```bash
# Generate full report
pytest --cov=app --cov-report=html --cov-report=term-missing

# Output shows missing lines:
# Name                     Stmts  Miss  Cover   Missing
# ------------------------------------------------
# app/extraction/domain/entities.py    45     0   100%
# app/extraction/domain/value_objects  20     0   100%
# app/extraction/application/use_cases 60     5    92%   45-47, 89-91
# app/reporting/domain/entities.py     30     0   100%
# ------------------------------------------------
# TOTAL                              500    50    85%
```

### Step 5: Identify Coverage Gaps

```bash
# Coverage by directory
pytest --cov=app/extraction --cov-report=term-missing

# Coverage for specific file
pytest --cov=app/extraction/domain --cov-report=term-missing

# View HTML report for interactive analysis
open htmlcov/app_extraction_domain_entities_py.html

# Check branch coverage specifically
pytest --cov=app --cov-report=term-missing:skip-covered
```

### Step 6: Exclude Lines Appropriately

```python
from typing import TYPE_CHECKING

if TYPE_CHECKING:  # pragma: no cover
    # Type checking only imports, never executed at runtime
    from myapp.domain.entities import Order


class BaseRepository(Protocol):
    """Protocol for repositories."""

    def save(self, entity: Entity) -> None:  # pragma: no cover
        """Abstract method, no implementation."""
        ...


def __repr__(self) -> str:  # pragma: no cover
    """Repr method, low value to test."""
    return f"Order(id={self.id})"


if __name__ == "__main__":  # pragma: no cover
    # CLI entry point, tested separately
    main()


@abstractmethod
def abstract_method(self) -> None:  # pragma: no cover
    """Abstract method, no implementation."""
    pass
```

### Step 7: Track Coverage Trends

```python
# Save coverage data to JSON for tracking
pytest --cov=app --cov-report=json

# Then analyze coverage.json to track improvements over time
import json

with open("coverage.json") as f:
    data = json.load(f)

total_coverage = data["totals"]["percent_covered"]
print(f"Total coverage: {total_coverage}%")

# Track per module
for module, coverage in data["files"].items():
    print(f"{module}: {coverage['summary']['percent_covered']}%")
```

### Step 8: Create Coverage Badges and Reports

```bash
# Generate coverage report that CI/CD can use
pytest --cov=app --cov-report=xml --cov-report=term

# Upload to coverage tracking services:
# - codecov.io
# - coveralls.io
# - codeclimate.com
```

### Step 9: Enforce Coverage in CI/CD

```yaml
# GitHub Actions example
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Install uv
        run: curl -LsSf https://astral.sh/uv/install.sh | sh

      - name: Install dependencies
        run: uv sync

      - name: Run tests with coverage
        run: uv run pytest --cov=app --cov-report=xml --cov-fail-under=80

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

### Step 10: Coverage-Driven Test Development

```python
# Process:
# 1. Run coverage before writing tests
# 2. Identify untested lines
# 3. Write tests to cover them
# 4. Re-run coverage to verify

from app.extraction.domain.value_objects import ProductTitle

# Before tests:
# ProductTitle: 20 lines, 0% covered

# Run tests:
# pytest --cov=app/extraction/domain --cov-report=term-missing

# See missing lines in output:
# ProductTitle: 20 lines, 5 missing → 75% covered

# Write tests for missing lines:
# - test_valid_title
# - test_title_too_long
# - test_immutability
# - test_equality
# - test_hashing

# After tests:
# ProductTitle: 20 lines, 0 missing → 100% covered
```

## Examples

### Example 1: Complete Coverage Configuration

```toml
[tool.pytest.ini_options]
addopts = [
    "--strict-markers",
    "--cov=app",
    "--cov-report=html",
    "--cov-rep

Related in Web Dev