Calidad de software y cobertura de pruebas
La cobertura de pruebas (Code Coverage) es una de las métricas más importantes en la calidad de software. Mide qué porcentaje del código se ejecuta a través de pruebas automatizadas. Sin embargo, una cobertura alta por sí sola no garantiza calidad: lo que realmente importa es qué y cómo se mide.
Resumen ejecutivo
La cobertura de pruebas cuantifica la proporción del código que se ejecuta mediante tests. Las métricas principales son Line Coverage, Branch Coverage y Function Coverage. Un valor de cobertura del 80% significa que el 80% de las líneas se alcanzó al menos una vez durante la ejecución de pruebas. Sin embargo, esto no dice nada sobre si los tests contienen las aserciones correctas.
Descripción técnica
Code Coverage mide el grado en que el código fuente de un programa se ejecuta mediante pruebas. Se expresa como porcentaje y puede medirse en varios niveles: líneas, sentencias, ramas (branches), funciones y caminos. Las herramientas de coverage instrumentan el código durante la ejecución y registran qué áreas se han atravesado. La métrica ayuda a identificar áreas sin pruebas, pero no garantiza ausencia de errores: una prueba puede ejecutar una línea sin verificar el comportamiento esperado.
Quién necesita cobertura de pruebas y por qué
La cobertura de pruebas es relevante para:
- Desarrolladores: para ver qué áreas del código aún no han sido probadas
- Equipos: para definir un umbral mínimo de calidad
- Ingenieros QA: para identificar áreas de riesgo
- Gerentes de proyecto: para evaluar la madurez de la estrategia de testing
- Cumplimiento normativo: en industrias reguladas (medicina, automoción, finanzas) los umbrales de cobertura suelen ser obligatorios
Métricas de cobertura en detalle
Line Coverage (Cobertura de líneas)
Mide cuántas líneas del código fuente se han ejecutado mediante pruebas. Es la métrica más simple y más utilizada.
function classify(score) {
if (score >= 90) return 'A'; // Línea 1
if (score >= 80) return 'B'; // Línea 2
if (score >= 70) return 'C'; // Línea 3
return 'F'; // Línea 4
}
Una prueba con classify(95) cubre solo la línea 1, lo que da 25% de Line Coverage. Pruebas con classify(95) y classify(65) cubren las líneas 1 y 4, lo que da 50%.
Branch Coverage (Cobertura de ramas)
Mide cuántas bifurcaciones (if/else, switch, ternary) se han cubierto mediante pruebas. Cada bifurcación tiene dos ramas: verdadera y falsa.
function classify(score) {
if (score >= 90) return 'A'; // Rama 1: true/false
if (score >= 80) return 'B'; // Rama 2: true/false
if (score >= 70) return 'C'; // Rama 3: true/false
return 'F';
}
Hay 6 ramas en total (3× true, 3× false). Una prueba con classify(95) cubre la rama 1-true, pero no la rama 1-false, lo que da 1/6 = 17% de Branch Coverage.
Function Coverage (Cobertura de funciones)
Mide cuántas funciones del código se han llamado al menos una vez. Es especialmente importante para módulos con muchas funciones pequeñas.
Path Coverage (Cobertura de caminos)
Mide cuántos posibles caminos de ejecución a través del código se han cubierto. Es la métrica más estricta, ya que considera todas las combinaciones de bifurcaciones. Con n bifurcaciones independientes hay 2^n caminos, por lo que la cobertura de caminos suele ser poco realista en código complejo.
Statement Coverage (Cobertura de sentencias)
Similar a Line Coverage, pero se refiere a sentencias individuales en lugar de líneas. Una línea puede contener varias sentencias.
Ejemplo práctico: Jest con Istanbul Coverage
Configuración
// package.json
{
"scripts": {
"test": "jest",
"test:coverage": "jest --coverage"
},
"jest": {
"collectCoverageFrom": [
"src/**/*.js",
"!src/**/*.spec.js",
"!src/index.js"
],
"coverageThreshold": {
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
}
}
}
Módulo de ejemplo
// src/utils/discount.js
export function calculateDiscount(price, customerType) {
if (typeof price !== 'number' || price < 0) {
throw new Error('Price must be a non-negative number');
}
switch (customerType) {
case 'premium':
return price * 0.8; // 20% de descuento
case 'vip':
return price * 0.7; // 30% de descuento
case 'standard':
return price; // Sin descuento
default:
throw new Error(`Unknown customer type: ${customerType}`);
}
}
Pruebas con cobertura completa de ramas
// src/utils/discount.spec.js
import { calculateDiscount } from './discount.js';
describe('calculateDiscount', () => {
test('premium customer gets 20% discount', () => {
expect(calculateDiscount(100, 'premium')).toBe(80);
});
test('vip customer gets 30% discount', () => {
expect(calculateDiscount(100, 'vip')).toBe(70);
});
test('standard customer gets no discount', () => {
expect(calculateDiscount(100, 'standard')).toBe(100);
});
test('throws on negative price', () => {
expect(() => calculateDiscount(-1, 'premium'))
.toThrow('Price must be a non-negative number');
});
test('throws on non-number price', () => {
expect(() => calculateDiscount('100', 'premium'))
.toThrow('Price must be a non-negative number');
});
test('throws on unknown customer type', () => {
expect(() => calculateDiscount(100, 'unknown'))
.toThrow('Unknown customer type: unknown');
});
});
Reporte de cobertura
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
discount | 100 | 100 | 100 | 100 |
----------|---------|----------|---------|---------|-------------------
Objetivos de cobertura: Qué valores tienen sentido
| Tipo de proyecto | Cobertura recomendada | Justificación |
|---|---|---|
| Librería / SDK | 90-100% | Alta estabilidad, muchos consumidores |
| Aplicación web | 70-80% | Pragmático, enfoque en lógica de negocio |
| Prototipo / MVP | 40-60% | Iteración rápida, tests para lógica central |
| Software regulado | 100% Branch | Obligatorio (IEC 62304, DO-178C) |
| Código heredado | 50%+ gradualmente | Incremento durante refactorización |
Regla práctica: 80% de Line Coverage con buenas aserciones vale más que 100% de cobertura sin aserciones.
Trampas comunes
1. 100% de cobertura sin aserciones
// Malo: la línea se ejecuta pero no se verifica nada
test('calculateDiscount runs', () => {
calculateDiscount(100, 'premium'); // Sin aserción
});
Cobertura: 100%, calidad: 0%.
2. Gaming de cobertura
Los desarrolladores escriben tests solo para alcanzar umbrales de cobertura sin aserciones significativas. Solución: usa Mutation Testing.
3. Usar ignore-pragmas excesivamente
/* istanbul ignore next */
export function complexFunction() { ... }
Cada ignore oculta código sin probar. Úsalo solo para código específico de plataforma o caminos de error deliberadamente excluidos.
4. Medir solo Line Coverage
Line Coverage es la métrica más débil. Branch Coverage detecta casos extremos sin testar que Line Coverage pasa por alto.
Mutation Testing: el siguiente nivel
Mutation Testing modifica el código de manera sistemática (por ejemplo, > por >=, + por -) y verifica si los tests capturan esos mutantes. Si un mutante sobrevive, significa que el test es insuficiente.
# Stryker Mutation Testing para JavaScript
npx stryker run
Ejemplos de mutantes:
if (score >= 90)→if (score > 90)— detecta tests que no verifican exactamente 90return price * 0.8→return price * 0.9— detecta tests sin assertions exactasreturn 'A'→return 'B'— detecta tests que no validan el valor de retorno
Herramientas disponibles
| Herramienta | Lenguaje | Métricas | Característica |
|---|---|---|---|
| Istanbul/nyc | JavaScript | Line, Branch, Func | Estándar en Jest |
| Coverage.py | Python | Line, Branch | Estándar en pytest |
| JaCoCo | Java | Line, Branch, Method | Estándar en Maven/Gradle |
| gcov | C/C++ | Line, Branch | Integrado en GCC |
| Stryker | JS/TS | Mutation Score | Mutation Testing |
| PIT | Java | Mutation Score | Mutation Testing |
Puntos clave para evaluación
- Diferencia entre Line, Branch, Path y Function Coverage
- Coverage es condición necesaria pero no suficiente para calidad
- Umbrales de coverage en pipelines CI/CD como quality gates
- Mutation Testing como complemento a la medición de coverage
- Herramientas de coverage: Istanbul, JaCoCo, Coverage.py, gcov
- 100% Coverage sin assertions no tiene valor
- ISO 25010: testabilidad como atributo de calidad
FAQ
1. ¿Line vs Branch Coverage?
2. ¿Es suficiente 100% Coverage?
3. ¿Cuál es el umbral recomendado?
4. ¿Qué es Mutation Testing?
5. ¿Qué es Path Coverage?
6. ¿Cómo configurar coverage en Jest?
7. ¿Qué es Istanbul?
8. ¿Cómo forzar coverage en CI/CD?
9. ¿Qué es Function Coverage?
10. ¿Qué es Coverage-Gaming?
11. ¿Herramientas para otros lenguajes?
12. ¿Qué es istanbul ignore?
13. ¿Qué es un LCOV-Report?
14. ¿Coverage en código legacy?
15. ¿Coverage vs Calidad de tests?
Continúa en la ruta de aprendizaje de Calidad de Software
El siguiente artículo en la ruta de aprendizaje de Calidad de Software aborda Clean Code y Principios SOLID — los fundamentos para código legible, mantenible y extensible.
Referencias
- https://istanbul.js.org/
- https://stryker-mutator.io/
- https://jestjs.io/docs/configuration#coveragethreshold-object
- https://iso25000.com/index.php/en/iso-25000-standards/iso-25010.html
Lecturas recomendadas sobre Calidad de Software
Si deseas profundizar en cobertura de tests, calidad de software y testing, te recomendamos los siguientes libros:
Keine Bücher für Kategorie "software-engineering" gefunden.



