Medir Cobertura de Tests con pytest-cov
Cómo medir y exigir umbrales de cobertura de tests en Python con pytest-cov, incluyendo branch coverage, reportes HTML, exclusiones e integración con CI.
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
Overview
pytest-cov es un plugin de pytest que integra la librería coverage.py. Mide qué líneas de tu código Python se ejecutan durante los tests y reporta el porcentaje. Puedes exigir umbrales mínimos, generar reportes HTML y trackear branch coverage (paths if/else) — no solo line coverage.
When to Use
- Medir qué porción de tu codebase está cubierta por tests
- Exigir un umbral mínimo de cobertura en CI (e.g., fallar builds por debajo de 80%)
- Identificar código muerto que nunca se ejecuta por ningún test
- Generar reportes HTML de cobertura para inspección visual de gaps
- Trackear branch coverage para asegurar que ambos lados de if/else estén testeados
When NOT to Use
- Perseguir 100% de cobertura como meta — 80-90% con buena calidad de test es mejor que 100% con tests triviales
- Medir cobertura para scripts o utilidades one-off — enfócate en código de producción testado
- Usar cobertura como la única métrica de calidad — cobertura alta con malas aserciones da falsa confianza
Solution
Setup
pip install pytest pytest-cov
Run básico de cobertura
pytest --cov=myapp tests/
Esto imprime un resumen en la terminal:
---------- coverage: platform linux, python 3.12 ----------
Name Stmts Miss Cover
-------------------------------------------
myapp/__init__.py 2 0 100%
myapp/models.py 45 5 89%
myapp/services.py 80 12 85%
myapp/api.py 60 8 87%
-------------------------------------------
TOTAL 187 25 87%
Exigir cobertura mínima
pytest --cov=myapp --cov-fail-under=80 tests/
Si la cobertura cae por debajo de 80%, pytest termina con un código non-zero, fallando CI.
Reporte HTML
pytest --cov=myapp --cov-report=html tests/
Abre htmlcov/index.html en un navegador. Las líneas verdes están cubiertas, las rojas no, con highlighting línea por línea.
Branch coverage
pytest --cov=myapp --cov-branch --cov-report=term-missing tests/
Branch coverage verifica que tanto el path if como el else de cada condicional se ejecuten. Line coverage solo puede miss branches donde la condición es siempre true o siempre false en los tests.
Configuración en pyproject.toml
[tool.pytest.ini_options]
addopts = "--cov=myapp --cov-report=term-missing --cov-report=html --cov-branch"
[tool.coverage.run]
source = ["myapp"]
branch = true
omit = [
"myapp/__init__.py",
"myapp/migrations/*",
"*/tests/*",
]
[tool.coverage.report]
show_missing = true
skip_covered = false
fail_under = 80
exclude_lines = [
"pragma: no cover",
"def __repr__",
"if TYPE_CHECKING:",
"raise NotImplementedError",
"if __name__ == .__main__.:",
]
Excluir líneas específicas
def get_config(key: str, default=None):
if key in os.environ:
return os.environ[key]
return default # pragma: no cover
El comentario # pragma: no cover le dice a coverage que ignore esa línea.
Excluir bloques enteros
if TYPE_CHECKING:
from myapp.models import User # pragma: no cover
Con la config exclude_lines de arriba, cualquier línea que matchee if TYPE_CHECKING: y todo lo que está debajo se excluye.
Cobertura para multiprocessing
# pyproject.toml
[tool.coverage.run]
concurrency = ["multiprocessing", "thread"]
Cobertura con ejecución paralela de tests
pytest -n auto --cov=myapp --cov-report=term-missing
Con pytest-xdist, cada worker escribe su propio archivo .coverage. Usa coverage combine antes de generar el reporte:
coverage combine
coverage report
coverage html
Variants
Usar coverage.py directamente (sin pytest)
coverage run -m pytest tests/
coverage report -m
coverage html
Coverage diff con diff-cover
pip install diff-cover
coverage xml
diff-cover coverage.xml --compare-branch=origin/main --html-report=coverage-diff.html
Esto muestra cobertura solo para las líneas cambiadas en la rama actual — útil para revisiones de PR.
Tendencias de cobertura con coverage-badge
pip install coverage-badge
coverage-badge -o coverage-badge.svg
Genera un SVG badge con el porcentaje de cobertura actual para tu README.
Best Practices
-
For a deeper guide, see Pytest in Production Guide.
-
Setea un umbral realista (80-85%) — muy alto fomenta tests triviales solo para alcanzar el número
-
Usa branch coverage junto con line coverage — atrapa paths else no testeados
-
Excluye archivos de migración,
__init__.pyy archivos de test de la medición de cobertura -
Usa
# pragma: no coverpara código solo de debug,__repr__y bloquesif __name__ -
Revisa los reportes de cobertura regularmente — encuentra gaps en paths críticos, no solo el porcentaje general
-
Genera XML de cobertura para herramientas de CI (SonarQube, Codecov, Coveralls)
Common Mistakes
- Perseguir 100% de cobertura: escribir tests triviales (
assert True) para cubrir líneas sin verificar comportamiento. - No usar branch coverage: line coverage de 100% puede aún miss branches
else. - Incluir archivos de test en la cobertura:
tests/debería excluirse — estás midiendo código de producción. - No combinar archivos de cobertura paralelos: con
pytest-xdist, cada worker escribe un archivo separado. Correcoverage combineantes de reportar. - Excluir demasiado: si excluyes cada línea difícil de testear, el número pierde sentido.
FAQ
¿Cuál es la diferencia entre line coverage y branch coverage?
Line coverage mide si una línea se ejecutó. Branch coverage mide si ambos paths de un condicional (if/else) se tomaron. Una línea con if x: puede tener 100% line coverage pero solo 50% branch coverage si x es siempre True en los tests.
¿Cómo excluyo un archivo entero de la cobertura?
En pyproject.toml:
[tool.coverage.run]
omit = ["myapp/legacy/*", "myapp/migrations/*"]
¿Cómo obtengo cobertura para un solo archivo de test?
pytest tests/test_models.py --cov=myapp.models --cov-report=term-missing
¿Puedo usar pytest-cov con Django o Flask?
Sí. Apunta --cov al paquete de tu proyecto:
pytest --cov=myproject --cov-report=html
Para Django, asegúrate de que DJANGO_SETTINGS_MODULE esté seteado en tu configuración de test.
¿Cómo fallo CI solo cuando la cobertura disminuye?
Usa diff-cover con --fail-under=100 para requerir 100% de cobertura en líneas cambiadas:
coverage xml
diff-cover coverage.xml --compare-branch=origin/main --fail-under=100
¿Cómo excluyo líneas de la cobertura?
Agrega # pragma: no cover para excluir líneas individuales. Usa # pragma: no cover <reason> para documentación. Configura exclusiones en .coveragerc:
[report]
exclude_lines =
pragma: no cover
def __repr__
raise NotImplementedError
if __name__ == .__main__.:
Excluye código de debug, métodos repr, y stubs de métodos abstractos. No excluyas paths de error handling — esos son críticos de testear.
¿Cómo mido branch coverage en lugar de line coverage?
Pasa --cov-branch a pytest-cov o setea branch = True en .coveragerc:
pytest --cov=myapp --cov-branch --cov-report=term-missing
Branch coverage reporta si tanto el path true como el false de cada condicional fueron ejecutados. Detecta else branches faltantes y paths de short-circuit evaluation que line coverage no detecta.
¿Cómo genero badges de cobertura para mi README?
Usa coverage-badge para generar badges SVG desde tu reporte de cobertura:
pip install coverage-badge
coverage-badge -o coverage.svg
Agrega el badge a tu README: . En CI, genera el badge como artifact y commitealo a una rama badges o subelo a un servicio de badges como shields.io.
¿Cómo manejo cobertura con multiprocessing?
Usa coverage con --concurrency=multiprocessing en .coveragerc:
[run]
concurrency = multiprocessing
parallel = True
Esto genera archivos de coverage data separados por proceso. Ejecuta coverage combine después de la suite de tests para mergeearlos. Sin esto, la cobertura de procesos child se pierde.
¿Cómo integro cobertura con GitHub Actions?
Agrega un step en tu workflow para ejecutar pytest con cobertura y subir el reporte:
- run: pytest --cov=myapp --cov-report=xml
- uses: codecov/codecov-action@v4
with:
file: ./coverage.xml
Codecov publica un comentario en PRs con el diff de cobertura y visualiza líneas no cubiertas. Usa fail_under en .coveragerc para fallear el job de CI si la cobertura baja de un threshold.
Recursos Relacionados
Pytest Fixtures and Parametrize
How to use pytest fixtures and @pytest.mark.parametrize to write data-driven tests with reusable setup logic across Python projects.
RecipeMeasure Test Coverage
How to measure, report, and enforce code coverage with branch and condition coverage using pytest-cov, nyc, and JaCoCo for meaningful quality gates.
RecipeMock External APIs with responses Library
How to mock HTTP API calls in Python tests using the responses library, including status codes, headers, JSON bodies, and error simulation.
RecipeProperty-Based Testing with Hypothesis
How to use Hypothesis for property-based testing in Python, generating hundreds of test cases automatically from strategies instead of writing them by hand.