Cuatro tipos comunes de cobertura de código

Aprende qué es la cobertura de código y descubre cuatro formas comunes de medirla.

¿Has escuchado la frase "cobertura de código"? En esta publicación, exploraremos qué es la cobertura de código en las pruebas y cuatro formas comunes de medirla.

¿Qué es la cobertura de código?

La cobertura de código es una métrica que mide el porcentaje de código fuente que ejecutan tus pruebas. Te ayuda a identificar áreas en las que es posible que no se realicen las pruebas adecuadas.

A menudo, el registro de estas métricas se ve de la siguiente manera:

Archivo % de estados de cuenta % de rama Funciones de% % de líneas Líneas descubiertas
file.js El 90% 100% El 90% 80% 89,256
coffee.js El 55.55% 80% 50% El 62.5% 10 a 11, 18

A medida que agregas nuevas funciones y pruebas, el aumento de los porcentajes de cobertura de código puede brindarte más confianza de que tu aplicación se probó de manera exhaustiva. Sin embargo, hay más por descubrir.

Cuatro tipos comunes de cobertura de código

Existen cuatro formas comunes de recopilar y calcular la cobertura de código: cobertura de función, línea, rama y declaración.

Cuatro tipos de cobertura de texto

Para ver cómo cada tipo de cobertura de código calcula su porcentaje, considera el siguiente ejemplo de código para calcular los ingredientes de café:

/* coffee.js */

export function calcCoffeeIngredient(coffeeName, cup = 1) {
  let espresso, water;

  if (coffeeName === 'espresso') {
    espresso = 30 * cup;
    return { espresso };
  }

  if (coffeeName === 'americano') {
    espresso = 30 * cup; water = 70 * cup;
    return { espresso, water };
  }

  return {};
}

export function isValidCoffee(name) {
  return ['espresso', 'americano', 'mocha'].includes(name);
}

Las pruebas que verifican la función calcCoffeeIngredient son las siguientes:

/* coffee.test.js */

import { describe, expect, assert, it } from 'vitest';
import { calcCoffeeIngredient } from '../src/coffee-incomplete';

describe('Coffee', () => {
  it('should have espresso', () => {
    const result = calcCoffeeIngredient('espresso', 2);
    expect(result).to.deep.equal({ espresso: 60 });
  });

  it('should have nothing', () => {
    const result = calcCoffeeIngredient('unknown');
    expect(result).to.deep.equal({});
  });
});

Puedes ejecutar el código y las pruebas en esta demostración en vivo o consultar el repositorio.

Cobertura de funciones

Cobertura de código: 50%

/* coffee.js */

export function calcCoffeeIngredient(coffeeName, cup = 1) {
  // ...
}

function isValidCoffee(name) {
  // ...
}

La cobertura de funciones es una métrica sencilla. Captura el porcentaje de funciones de tu código al que llaman las pruebas.

En el ejemplo de código, hay dos funciones: calcCoffeeIngredient y isValidCoffee. Las pruebas solo llaman a la función calcCoffeeIngredient, por lo que la cobertura de la función es del 50%.

Cobertura de línea

Cobertura de código: 62.5%

/* coffee.js */

export function calcCoffeeIngredient(coffeeName, cup = 1) {
  let espresso, water;

  if (coffeeName === 'espresso') {
    espresso = 30 * cup;
    return { espresso };
  }

  if (coffeeName === 'americano') {
    espresso = 30 * cup; water = 70 * cup;
    return { espresso, water };
  }

  return {};
}

export function isValidCoffee(name) {
  return ['espresso', 'americano', 'mocha'].includes(name);
}

La cobertura de líneas mide el porcentaje de líneas de código ejecutable que ejecutó el conjunto de pruebas. Si una línea de código no se ejecuta, significa que no se probó alguna parte del código.

El ejemplo de código tiene ocho líneas de código ejecutable (resaltadas en rojo y verde), pero las pruebas no ejecutan la condición americano (dos líneas) ni la función isValidCoffee (una línea). Esto da como resultado una cobertura de línea del 62.5%.

Ten en cuenta que la cobertura de línea no considera las declaraciones de declaración, como function isValidCoffee(name) y let espresso, water;, ya que no son ejecutables.

Cobertura de la sucursal

Cobertura de código: 80%

/* coffee.js */

export function calcCoffeeIngredient(coffeeName, cup = 1) {
  // ...

  if (coffeeName === 'espresso') {
    // ...
    return { espresso };
  }

  if (coffeeName === 'americano') {
    // ...
    return { espresso, water };
  }

  return {};
}

La cobertura de ramas mide el porcentaje de ramas o puntos de decisión ejecutados en el código, como sentencias o bucles. Determina si las pruebas examinan las ramas verdadera y falsa de las declaraciones condicionales.

Hay cinco ramas en el ejemplo de código:

  1. Llamando a calcCoffeeIngredient con solo coffeeName Marca de tilde.
  2. Llamando a calcCoffeeIngredient con coffeeName y cup Marca de tilde.
  3. Café exprés Marca de tilde.
  4. Café americano X.
  5. Otro café Marca de tilde.

Las pruebas abarcan todas las ramas, excepto la condición Coffee is Americano. Por lo tanto, la cobertura de las sucursales es del 80%.

Cobertura del resumen

Cobertura de código: 55.55%

/* coffee.js */

export function calcCoffeeIngredient(coffeeName, cup = 1) {
  let espresso, water;

  if (coffeeName === 'espresso') {
    espresso = 30 * cup;
    return { espresso };
  }

  if (coffeeName === 'americano') {
    espresso = 30 * cup; water = 70 * cup;
    return { espresso, water };
  }

  return {};
}

export function isValidCoffee(name) {
  return ['espresso', 'americano', 'mocha'].includes(name);
}

La cobertura de estados de cuenta mide el porcentaje de sentencias del código que ejecutan las pruebas. A primera vista, es posible que te preguntes: “¿No es lo mismo que la cobertura de líneas?”. De hecho, la cobertura de instrucciones es similar a la cobertura de líneas, pero tiene en cuenta líneas de código únicas que contienen varias sentencias.

En el ejemplo de código, hay ocho líneas de código ejecutable, pero hay nueve instrucciones. ¿Puedes encontrar la línea que contiene dos afirmaciones?

Verifica tu respuesta

Es la siguiente línea: espresso = 30 * cup; water = 70 * cup;

Las pruebas cubren solo cinco de los nueve enunciados; por lo tanto, la cobertura de los resúmenes es de un 55.55%.

Si siempre escribes un estado de cuenta por línea, la cobertura de tu línea será similar a la de tu resumen.

¿Qué tipo de cobertura de código deberías elegir?

La mayoría de las herramientas de cobertura de código incluyen estos cuatro tipos de cobertura de código común. La elección de la métrica de cobertura de código que se debe priorizar depende de los requisitos específicos del proyecto, las prácticas de desarrollo y los objetivos de prueba.

En general, la cobertura de los enunciados es un buen punto de partida porque es una métrica simple y fácil de entender. A diferencia de la cobertura de instrucciones, la cobertura de ramas y la cobertura de funciones miden si las pruebas llaman a una condición (rama) o a una función. Por lo tanto, son una progresión natural después de la cobertura de la instrucción.

Una vez que hayas alcanzado una alta cobertura del estado de cuenta, puedes pasar a la cobertura de sucursales y la cobertura de funciones.

¿La cobertura de la prueba es la misma que la de código?

No. A menudo, la cobertura de la prueba y la de código se confunden, pero son diferentes:

  • Cobertura de la prueba: Es la métrica acualitativa que mide qué tan bien el paquete de pruebas cubre las funciones del software. Ayuda a determinar el nivel de riesgo involucrado.
  • Cobertura de código: Es una métrica cuantitativa que mide la proporción de código ejecutado durante las pruebas. Se trata de la cantidad de código que abarcan las pruebas.

Aquí hay una analogía simplificada: imagina una aplicación web como una casa.

  • La cobertura de pruebas mide si las pruebas cubren las habitaciones de la casa.
  • La cobertura de código mide cuánto de la casa han pasado las pruebas.

Una cobertura de código al 100% no implica que no haya errores.

Si bien es muy conveniente lograr una alta cobertura de código durante las pruebas, una cobertura de código al 100% no garantiza la ausencia de errores o fallas en tu código.

Una forma sin sentido de lograr una cobertura de código al 100%

Considera la siguiente prueba:

/* coffee.test.js */

// ...
describe('Warning: Do not do this', () => {
  it('is meaningless', () => { 
    calcCoffeeIngredient('espresso', 2);
    calcCoffeeIngredient('americano');
    calcCoffeeIngredient('unknown');
    isValidCoffee('mocha');
    expect(true).toBe(true); // not meaningful assertion
  });
});

Esta prueba alcanza el 100% de cobertura de funciones, líneas, ramas y sentencias, pero no tiene sentido porque en realidad no prueba el código. La aserción expect(true).toBe(true) siempre se pasará independientemente de si el código funciona correctamente.

Una métrica deficiente es peor que no tener ninguna

Una métrica deficiente puede darte una falsa sensación de seguridad, lo cual es peor que no tener ninguna métrica. Por ejemplo, si tienes un paquete de pruebas que alcanza el 100% de cobertura de código, pero las pruebas no tienen ningún significado, es posible que recibas la falsa sensación de seguridad de que tu código está bien probado. Si borras o rompes una parte del código de la aplicación por accidente, las pruebas serán exitosas aunque la aplicación ya no funcione correctamente.

Para evitar esta situación, haz lo siguiente:

  • Revisión de prueba. Escribe y revisa pruebas para asegurarte de que sean significativas y prueba el código en diferentes situaciones.
  • Usa la cobertura de código como guía, no como la única medición de la eficacia de las pruebas o de la calidad del código.

Cómo usar la cobertura de código en diferentes tipos de pruebas

Analicemos con mayor detalle cómo puedes usar la cobertura de código con los tres tipos comunes de prueba:

  • Pruebas de unidades. Son el mejor tipo de prueba para recopilar cobertura de código, ya que están diseñados para abarcar varias rutas de prueba y situaciones pequeñas.
  • Pruebas de integración. Pueden ayudar a recopilar cobertura de código para pruebas de integración, pero úsalas con precaución. En este caso, calculas la cobertura de una porción más grande del código fuente, y puede ser difícil determinar qué pruebas cubren realmente qué partes del código. No obstante, calcular la cobertura de código de las pruebas de integración puede ser útil para los sistemas heredados que no tienen unidades bien aisladas.
  • Pruebas de extremo a extremo (E2E). Medir la cobertura de código para pruebas de extremo a extremo es difícil y desafiante debido a la naturaleza compleja de estas pruebas. En lugar de usar la cobertura de código, la cobertura de requisitos podría ser la mejor opción. Esto se debe a que el enfoque de las pruebas de E2E es cubrir los requisitos de la prueba, no el código fuente.

Conclusión

La cobertura de código puede ser una métrica útil para medir la eficacia de tus pruebas. Puede ayudarte a mejorar la calidad de tu aplicación, ya que garantiza que la lógica crucial en tu código esté bien probada.

Sin embargo, recuerda que la cobertura de código es solo una métrica. Asegúrate de considerar también otros factores, como la calidad de tus pruebas y los requisitos de tu aplicación.

Apuntar a una cobertura de código del 100% no es el objetivo. En su lugar, debes usar la cobertura de código junto con un plan de pruebas completo que incorpore una variedad de métodos de prueba, incluidas pruebas de unidades, de integración, de extremo a extremo y manuales.

Mira el ejemplo de código completo y las pruebas con una buena cobertura de código. También puedes ejecutar el código y las pruebas con esta demostración en vivo.

/* coffee.js - a complete example */

export function calcCoffeeIngredient(coffeeName, cup = 1) {
  if (!isValidCoffee(coffeeName)) return {};

  let espresso, water;

  if (coffeeName === 'espresso') {
    espresso = 30 * cup;
    return { espresso };
  }

  if (coffeeName === 'americano') {
    espresso = 30 * cup; water = 70 * cup;
    return { espresso, water };
  }

  throw new Error (`${coffeeName} not found`);
}

function isValidCoffee(name) {
  return ['espresso', 'americano', 'mocha'].includes(name);
}
/* coffee.test.js - a complete test suite */

import { describe, expect, it } from 'vitest';
import { calcCoffeeIngredient } from '../src/coffee-complete';

describe('Coffee', () => {
  it('should have espresso', () => {
    const result = calcCoffeeIngredient('espresso', 2);
    expect(result).to.deep.equal({ espresso: 60 });
  });

  it('should have americano', () => {
    const result = calcCoffeeIngredient('americano');
    expect(result.espresso).to.equal(30);
    expect(result.water).to.equal(70);
  });

  it('should throw error', () => {
    const func = () => calcCoffeeIngredient('mocha');
    expect(func).toThrowError(new Error('mocha not found'));
  });

  it('should have nothing', () => {
    const result = calcCoffeeIngredient('unknown')
    expect(result).to.deep.equal({});
  });
});