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 System | Integration |
|---|---|
| GitHub Actions | codecov/codecov-action@v4 uploads coverage.xml |
| GitLab CI | Set coverage: '/TOTAL.*\s+(\d+%)$/' regex |
| Codecov | codecov --token=TOKEN after test run |
| SonarQube | Configure 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