Skip to content
IRC-CodingIRC-Coding
TestabdeckungCode CoverageLine CoverageBranch CoverageMutation TestingJestIstanbulCalidad de Software

Code Coverage y Testabdeckung: Métricas y Estrategias

Guía completa sobre Code Coverage: Line, Branch, Path Coverage, Mutation Testing, objetivos y ejemplos prácticos con Jest e Istanbul.

S

schutzgeist

7 min read
Code Coverage y Testabdeckung: Métricas y Estrategias

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 proyectoCobertura recomendadaJustificación
Librería / SDK90-100%Alta estabilidad, muchos consumidores
Aplicación web70-80%Pragmático, enfoque en lógica de negocio
Prototipo / MVP40-60%Iteración rápida, tests para lógica central
Software regulado100% BranchObligatorio (IEC 62304, DO-178C)
Código heredado50%+ gradualmenteIncremento 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 90
  • return price * 0.8return price * 0.9 — detecta tests sin assertions exactas
  • return 'A'return 'B' — detecta tests que no validan el valor de retorno

Herramientas disponibles

HerramientaLenguajeMétricasCaracterística
Istanbul/nycJavaScriptLine, Branch, FuncEstándar en Jest
Coverage.pyPythonLine, BranchEstándar en pytest
JaCoCoJavaLine, Branch, MethodEstándar en Maven/Gradle
gcovC/C++Line, BranchIntegrado en GCC
StrykerJS/TSMutation ScoreMutation Testing
PITJavaMutation ScoreMutation 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?

Line mide líneas, Branch mide ramificaciones (true/false). Branch es más estricto.

2. ¿Es suficiente 100% Coverage?

No, sin assertions 100% Coverage no tiene valor.

3. ¿Cuál es el umbral recomendado?

Web apps 70-80%, librerías 90-100%.

4. ¿Qué es Mutation Testing?

Modifica el código de forma sistemática y verifica si los tests capturan los mutantes.

5. ¿Qué es Path Coverage?

Mide todos los caminos de ejecución posibles. La métrica más estricta, frecuentemente impráctica.

6. ¿Cómo configurar coverage en Jest?

Establece coverageThreshold en jest.config.js.

7. ¿Qué es Istanbul?

Herramienta de coverage estándar para JavaScript, utilizada por Jest.

8. ¿Cómo forzar coverage en CI/CD?

Sí, como quality gate contra código sin testar.

9. ¿Qué es Function Coverage?

Mide cuántas funciones han sido invocadas.

10. ¿Qué es Coverage-Gaming?

Tests sin assertions solo para alcanzar umbrales. Solución: Mutation Testing.

11. ¿Herramientas para otros lenguajes?

Coverage.py (Python), JaCoCo (Java), gcov (C/C++).

12. ¿Qué es istanbul ignore?

Excluye código de la medición de coverage, úsalo con moderación.

13. ¿Qué es un LCOV-Report?

Formato estándar para datos de coverage para integración en CI/CD.

14. ¿Coverage en código legacy?

Escribe tests caracterizadores, refactoriza progresivamente.

15. ¿Coverage vs Calidad de tests?

Coverage mide ejecución, la calidad de tests mide detección de fallos.

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

  1. https://istanbul.js.org/
  2. https://stryker-mutator.io/
  3. https://jestjs.io/docs/configuration#coveragethreshold-object
  4. 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.

Volver al blog
Share:

Entradas relacionadas