Cuatro tipos comunes de cobertura de código

Obtén información sobre qué es la cobertura de código y descubre cuatro formas comunes de medirla.

¿Escuchaste 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 las áreas en las que es posible que no se realicen pruebas adecuadas.

A menudo, el registro de estas métricas tiene el siguiente aspecto:

Archivo Sentencias% % de rama Funciones% % 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-11 y 18

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

Cuatro tipos comunes de cobertura de código

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

Cuatro tipos de cobertura de texto.

Para ver cómo cada tipo de cobertura de código calcula su porcentaje, ten en cuenta 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 la función

Cobertura de código: 50%

/* coffee.js */

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

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

La cobertura de funciones es una métrica directa. Captura el porcentaje de funciones de tu código a las 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ínea mide el porcentaje de líneas de código ejecutables que ejecutó el paquete 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 if o bucles. Determina si las pruebas examinan las ramas verdadera y falsa de las sentencias condicionales.

Hay cinco ramas en el ejemplo de código:

  1. Llamando a calcCoffeeIngredient con solo coffeeName Marca de verificación.
  2. Llamando a calcCoffeeIngredient con coffeeName y cup Marca de verificación.
  3. El café es espresso Marca de verificación.
  4. El café es americano Marca X.
  5. Otro café Marca de verificación.

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 estado de cuenta

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 resúmenes mide el porcentaje de declaraciones de tu código que ejecutan las pruebas. A primera vista, podrías preguntarte, “¿no es lo mismo que la cobertura de línea?” De hecho, la cobertura de instrucciones es similar a la cobertura de línea, pero tiene en cuenta líneas de código únicas que contienen varias instrucciones.

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

Verifica tu respuesta

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

Las pruebas cubren solo cinco de las nueve instrucciones; por lo tanto, la cobertura de la declaración es del 55.55%.

Si siempre escribes un enunciado por línea, el alcance de la línea será similar al de la cobertura del 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 pruebas.

En general, la cobertura de resúmenes es un buen punto de partida porque es una métrica simple y fácil de entender. A diferencia de la cobertura de sentencias, la cobertura de ramas y 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 sentencia.

Una vez que hayas logrado una alta cobertura de resúmenes, podrás pasar a la cobertura de las ramas y de las funciones.

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

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

  • Cobertura de pruebas: Es una métrica cualitativa 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 que se ejecuta durante las pruebas. Se trata de la cantidad de código que cubren las pruebas.

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

  • La cobertura de las pruebas mide qué tan bien cubren las habitaciones de la casa.
  • La cobertura de código mide cuánto de la casa pasaron las pruebas.

Que el 100% de cobertura de código no significa que no haya errores.

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

Una forma sin sentido de lograr una cobertura de código del 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
  });
});

Con esta prueba, se obtiene una cobertura del 100% 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 completará sin importar si el código funciona correctamente.

Una métrica incorrecta es peor que ninguna métrica

Una métrica incorrecta puede darte una falsa sensación de seguridad, lo que es peor que no tener ninguna métrica. Por ejemplo, si tienes un paquete de pruebas que alcanza una cobertura de código del 100%, pero las pruebas no tienen sentido, es posible que tengas una falsa sensación de que el código se probó correctamente. Si borras o rompes una parte del código de la aplicación por accidente, las pruebas se aprobarán de todos modos, aunque la aplicación ya no funcione correctamente.

Para evitar esta situación, haz lo siguiente:

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

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

Analicemos con más 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 situaciones pequeñas y rutas de prueba.
  • Pruebas de integración. Pueden ayudar a recopilar la cobertura de código para las pruebas de integración, pero úsalas con precaución. En este caso, calculas la cobertura de una parte 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 las pruebas E2E es difícil y difícil 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, ya que el enfoque de las pruebas E2E es cubrir los requisitos de la prueba, no enfocarse en 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 se haya probado correctamente.

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

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

Mira el ejemplo de código completo y las pruebas con 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({});
  });
});