Medir Cobertura de Test
Cómo medir, reportar y hacer cumplir la cobertura de código con branch y condition coverage usando pytest-cov, nyc y JaCoCo para quality gates significativos.
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.
Descripción General
La cobertura de código mide qué líneas, branches y condiciones fueron ejecutadas durante los tests. Es un proxy útil para código no testeado, pero no una medida de calidad de test — 100% de cobertura sin assertions es meaningless. Esta receta muestra cómo recolectar, reportar y configurar thresholds de cobertura significativos sin crear incentivos perversos.
Cuándo Usar
- Necesitas visibilidad sobre qué rutas de código carecen de ejecución de test
- Los pipelines de CI necesitan una puerta para prevenir código no testeado de ser mergeado
- Estás refactorizando código legacy y quieres asegurar que los cambios nuevos están testeados
- Los equipos necesitan una métrica compartida para rastrear progreso de testing con el tiempo
- Quieres identificar código muerto que nunca se ejecuta en producción o tests
Cuándo NO Usar
- La cobertura se trata como un objetivo (ej. “debe ser 90%”) en lugar de una guía — esto lleva a tests sin assertions
- El codebase es un prototipo o spike que será descartado — la cobertura no añade valor
- Estás testeando código generado, boilerplate de framework o archivos de configuración
- El equipo optimiza porcentaje de cobertura sobre encontrar bugs reales
Implementación Paso a Paso
Python (pytest-cov)
# Instalar
pip install pytest-cov
# Ejecutar con reporte de terminal
pytest --cov=myproject --cov-report=term-missing tests/
# Generar reporte HTML
pytest --cov=myproject --cov-report=html --cov-report=xml tests/
# Fallar bajo threshold (hecho cumplir en CI)
pytest --cov=myproject --cov-fail-under=80 tests/
# Branch coverage (rastrea si if/else ambos tomados)
pytest --cov=myproject --cov-branch tests/
# Configuración pyproject.toml
[tool.coverage.run]
source = ["myproject"]
branch = true
omit = [
"*/tests/*",
"*/migrations/*",
"*/venv/*",
]
[tool.coverage.report]
precision = 2
fail_under = 80
skip_covered = true
show_missing = true
[tool.coverage.html]
directory = "htmlcov"
[tool.coverage.xml]
output = "coverage.xml"
# Ejecutando en CI con múltiples markers
pytest -m "not slow" --cov=myproject --cov-report=xml --cov-fail-under=80
JavaScript (nyc / c8)
# c8 es la moderna herramienta de cobertura nativa V8 rápida
npm install --save-dev c8
# Ejecutar tests con cobertura
npx c8 npm test
# Reporte HTML
npx c8 --reporter=html --reporter=text npm test
# Fallar bajo threshold
npx c8 --check-coverage --lines 80 --functions 80 --branches 75 npm test
# Excluir archivos de cobertura
npx c8 --exclude="src/**/*.test.js" --exclude="src/vendor/**" npm test
// package.json
{
"scripts": {
"test": "vitest run",
"test:coverage": "vitest run --coverage"
},
"devDependencies": {
"@vitest/coverage-v8": "^1.0.0",
"vitest": "^1.0.0"
}
}
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'json'],
lines: 80,
functions: 80,
branches: 75,
statements: 80,
exclude: [
'**/*.test.ts',
'**/tests/**',
'**/node_modules/**',
'**/vendor/**'
]
}
}
});
Java (JaCoCo)
<!-- pom.xml -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.11</version>
<executions>
<execution>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>check</id>
<goals>
<goal>check</goal>
</goals>
<configuration>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>LINE</counter>
<value>COVEREDRATIO</value>
<minimum>0.80</minimum>
</limit>
<limit>
<counter>BRANCH</counter>
<value>COVEREDRATIO</value>
<minimum>0.75</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
# Generar reporte
mvn jacoco:report
# Verificar thresholds
mvn jacoco:check
# Generar badge para README
mvn jacoco:report && cat target/site/jacoco/index.html | grep -oP 'Total[^%]+%'
Integración CI
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install pytest pytest-cov
- run: pytest --cov=myproject --cov-report=xml --cov-fail-under=80
- uses: codecov/codecov-action@v3
with:
files: ./coverage.xml
Mejores Prácticas
- Mide branch coverage, no solo line coverage. Una sola línea con
if x:reporta como cubierta si la rama true se ejecuta, incluso si la rama false nunca se testea. Branch coverage detecta esto. - Configura thresholds por módulo, no globalmente. La lógica de negocio core debería tener thresholds más altos (85-90%) que el código glue de UI o archivos auto-generados (50-60%).
- Excluye código de infraestructura de los objetivos. Migraciones de base de datos, clientes gRPC generados y archivos de config no deberían contar contra tu métrica de cobertura.
- Rastrea tendencias de cobertura, no números absolutos. Una caída de 5% en un PR es más accionable que “estamos en 82% hoy”.
- Revisa líneas no cubiertas en PRs, no solo el porcentaje. Un bot de comentarios que lista las 3 líneas no cubiertas es más útil que un checkmark rojo al 79%.
Errores Comunes
- Hacer cumplir 100% de cobertura. Incentiva tests que ejecutan código sin asertar comportamiento, o anotaciones
@excludepara gamear la métrica. - Solo medir line coverage. Una función con 10 branches puede mostrar 100% de line coverage mientras solo 2 branches están testeados.
- Incluir archivos de test en cobertura. Utilidades de test y clases mock inflan el número y ocultan cobertura de producción faltante.
- Comparar cobertura entre lenguajes. Python branch coverage y Java line coverage no son métricas comparables — rastrea tendencias dentro de cada codebase.
- Ignorar cobertura en tests de integración. Los tests de integración lentos a menudo cubiertan las rutas más importantes; excluirlos de cobertura oculta gaps reales.