pytest Coverage

Installation & Basic Usage

pip install pytest-cov # Run tests with coverage pytest --cov=myapp tests/ # Specify source package and output format pytest --cov=src --cov-report=term-missing tests/ # Multiple packages pytest --cov=myapp --cov=utils --cov-report=html tests/ # Short flags pytest --cov=. --cov-report=term # With branch coverage pytest --cov=myapp --cov-branch tests/ # Fail if coverage drops below threshold pytest --cov=myapp --cov-fail-under=85 tests/

coverage.ini / .coveragerc / pyproject.toml

# .coveragerc [run] source = myapp branch = True omit = */migrations/* */tests/* */conftest.py setup.py myapp/__main__.py [report] show_missing = True skip_empty = True precision = 2 fail_under = 85 [html] directory = htmlcov title = My App Coverage [xml] output = coverage.xml --- # pyproject.toml (modern approach) [tool.coverage.run] source = ["myapp"] branch = true omit = ["*/migrations/*", "*/tests/*"] [tool.coverage.report] fail_under = 85 show_missing = true [tool.pytest.ini_options] addopts = "--cov=myapp --cov-report=html --cov-branch"

Branch Coverage

# Enable branch coverage — measures if/else path coverage pytest --cov=myapp --cov-branch tests/ # Example output showing branch misses: # Name Stmts Miss Branch BrPart Cover # --------------------------------------------------------- # myapp/utils.py 20 2 8 3 82% # myapp/models.py 45 0 12 0 100% # Branch coverage in .coveragerc [run] branch = True # What branch coverage catches: # def process(x): # if x > 0: # branch: x>0 True AND False # return x * 2 # only True branch was tested = partial # return 0 # pragma: no branch — exclude specific branches def _internal(x): if x is None: # pragma: no branch raise ValueError("x cannot be None")

Coverage Reports

# Terminal report with missing line numbers pytest --cov=myapp --cov-report=term-missing tests/ # HTML report (opens in browser) pytest --cov=myapp --cov-report=html tests/ # Output at htmlcov/index.html # XML report (for CI/SonarQube) pytest --cov=myapp --cov-report=xml tests/ # Output at coverage.xml # JSON report pytest --cov=myapp --cov-report=json tests/ # Multiple reports at once pytest --cov=myapp \ --cov-report=term-missing \ --cov-report=html:htmlcov \ --cov-report=xml:coverage.xml \ tests/ # Combine coverage from multiple test runs coverage run -m pytest tests/unit/ coverage run -a -m pytest tests/integration/ # -a appends coverage report

Omit Patterns & pragma: no cover

# .coveragerc omit patterns [run] omit = */site-packages/* */migrations/versions/* */tests/* */__pycache__/* myapp/dev_tools.py # In code — exclude lines or blocks def unreachable(): # pragma: no cover pass class MetaClass(type): def __new__(cls, *args, **kwargs): # pragma: no cover return super().__new__(cls, *args, **kwargs) # Exclude entire files via .coveragerc [report] exclude_lines = pragma: no cover def __repr__ if TYPE_CHECKING: raise NotImplementedError if __name__ == .__main__.: \.\.\. pass

CI Integration

CI SystemIntegration
GitHub Actionscodecov/codecov-action@v4 uploads coverage.xml
GitLab CISet coverage: '/TOTAL.*\s+(\d+%)$/' regex
Codecovcodecov --token=TOKEN after test run
SonarQubeConfigure sonar.python.coverage.reportPaths=coverage.xml
# .github/workflows/test.yml - name: Run tests with coverage run: pytest --cov=myapp --cov-report=xml --cov-fail-under=80 - name: Upload coverage uses: codecov/codecov-action@v4 with: files: ./coverage.xml fail_ci_if_error: true