Young programmer or IT specialist satisfied with her work done_Code Coverage: Asegura la Calidad en el Testeo de Desarrollos

Code Coverage: Asegura la Calidad en el Testeo de Desarrollos

El desarrollo de aplicaciones requiere un testeo continuo y completo. Cuando no testeamos funcionalidades y casuísticas damos pie a la aparición de bugs. Con la Integración Continua (CI), implementamos la automatización de pruebas que se ejecutan como parte de la pipeline.

Además de superar las pruebas automáticas, debemos asegurarnos de que son completas para cubrir todas las posibilidades.

Para conseguir esta garantía usamos Code Coverage

¿Qué es Code Coverage?

Code Coverage es una métrica que podemos analizar después de realizar pruebas automáticas. Esta métrica indica qué parte del total del código ha sido realmente testeado por las pruebas. Es decir, dadas las pruebas automáticas, sacamos métricas de qué parte del código ha sido ejecutado realmente con ellas.   

Hay diferentes tipos de Code Coverage, en función de lo que analizemos. Para poder entenderlo mejor, usaremos un ejemplo de código fuente. 

/* 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); 
} 

 

En este código definimos un par de simples funciones. Una que devuelve ingredientes según el tipo de café y otra que indica qué tipos de café aceptamos como válidos.  

Sobre este código, realizamos estos test automáticos: 

/* 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({}); 
  }); 
}); 

En la prueba, solicitamos ingredientes para 2 espressos (debería darnos 60 en total, ya que cada uno lleva 30). Luego, pedimos ingredientes para un café ‘unknown’, que debería darnos un resultado vacío.

Esta prueba está testeando el código anterior, pero ¿qué Code Coverage está haciendo del mismo?  

Line Coverage

Para empezar, tenemos la métrica de Line Coverage. Esta métrica simplemente calcula el total de líneas ejecutables del código original, y el total de líneas que hemos ejecutado realmente con la prueba. En el código original, las líneas ejecutables son las resaltadas: 

/* coffee.js */ 
 
export function calcCoffeeIngredient(coffeeName, cup = 1) { 
  let espresso, water; 
 
 #14C8BE
  } 
 
  if (coffeeName === 'americano') { 
    espresso = 30 * cup; water = 70 * cup; 
    return { espresso, water }; 
  } 
 
  return {}; 
} 
 
export function isValidCoffee(name) { 
  return ['espresso', 'americano', 'mocha'].includes(name); 
} 

 

Al realizar esta prueba, en la primera parte miramos si el nombre es ‘espresso’, entramos en la primera condición y acabamos la prueba. En la segunda parte miramos si el nombre es ‘espresso’, no lo es y miramos si es ‘americano’, no lo es y devolvemos un conjunto vacío. 

Por tanto, en la prueba no estamos accediendo al código de la segunda condición (al que entramos si es ‘americano’). Y tampoco probamos si el nombre del tipo de café es válido, que se comprueba en otra función. En total hemos pasado por 5 de las 8 líneas ejecutables, lo que nos da un Line Coverage de 62.5%.  

Line Coverage no considera líneas que sean de definición y no se ejecuten. Line Coverage da una primera aproximación buena de que el código se está ejecutando, pero no es suficiente. Como vemos, hay una función entera que no ha sido testeada. Y con sólo Line Coverage podríamos pensar que sí que la testeamos, pero al 62.5%. Para tener una visión más completa, hemos de usar otros tipos de Coverage para ver por ejemplo cuantas funciones han sido testeadas.  

Statement Coverage 

Este tipo de Coverage nos indica los statements ejecutados dentro del código. De entrada, puede parecer lo mismo que el Line Coverage, y muchas veces lo es, pero se puede dar el caso de que una línea tenga más de un statement. Por ejemplo, en nuestro código, esta línea: 

    espresso = 30 * cup; water = 70 * cup; 

 
Aquí en una sola línea tenemos dos statements. Lo que nos deja el total de statements a 9, y nuestra prueba sólo ejecuta 5, dejando un Coverage del 55.55%.  

Function Coverage 

Function Coverage nos indica el total de funciones declaradas en el código que son realmente llamadas por las pruebas.  

En nuestro caso, tenemos dos: 

export function calcCoffeeIngredient(coffeeName, cup = 1)  
 

export function isValidCoffee(name) 

Dadas estas dos funciones, la prueba realmente sólo llama a una de ellas. El Function Coverage es del 50%.  

Si la prueba llamase a ambas funciones tendríamos un 100% de Function Coverage. Pero internamente podría ser que trozos enteros de código no se estuviesen consultando, dado que usamos condicionales y bucles.  

Branch Coverage

El Branch Coverage se encarga de comprobar, dentro del código, cuantas secciones separadas por condiciones o bucles realmente se han testeado. En el código anterior, tenemos las siguientes ramas: 

export function calcCoffeeIngredient(coffeeName, cup = 1) { 
  // ... 
 
  if (coffeeName === 'espresso') { 
    // ... 
    return { espresso }; 
  } 
 
  if (coffeeName === 'americano') { 
    // ... 
    return { espresso, water }; 
  } 
 
  return {}; 
} 

Dadas estas ramas, tenemos las siguientes opciones:  

  1. Llamar a la función con sólo el nombre del café  
  1. Llamar a la función con el nombre del café y el número de tazas. 
  1. Que el café sea ‘espresso’ 
  1. Que el café sea ‘americano’. 
  1. Que el café sea de otro tipo. 

La prueba que hemos realizado pasa por las ramas 1,2,3 y 5, quedando la 4 sin probar. Por tanto el Branch Coverage es del 80%.  

Hay otros tipos de Coverage, por ejemplo algunos que también intentan evaluar la complejidad de un código y ponderar el % de complejidad ejecutado, para dar más peso a código más complejo. Pero estos 4 que hemos comentado son básicos y comunes en muchas herramientas de Code Coverage.  

En función del lenguaje de programación y entorno habrá que usar diferentes herramientas para sacar las métricas y obtener valores, no hay una solución única al respecto.  

Code Coverage y Continuous Integration 

Como hemos podido ver, sólo un tipo de Coverage daría una idea parcial. Si sólo contamos líneas o statements, puede ser que las pruebas no estén accediendo a un trozo de código o función vital, pero de menor número de líneas, dando la apariencia de que hemos probado mucho código, pero faltando funcionalidades básicas. Mirando sólo ramas nos podemos dejar funciones, y mirando sólo funciones nos podemos dejar ramas internas importantes.  

Durante la CI deberemos usar herramientas que nos permitan calcular el Code Coverage que estamos realizando en una aplicación y nos indiquen varias métricas de Coverage. Dados estos resultados, hemos de añadir en la pipeline condiciones que paren la ejecución de la misma sin un mínimo de Coverage en las métricas obtenidas.  

Cuando se incluyan suficientes pruebas para que el Coverage augmente y la pipeline no falle, podremos seguir con la ejecución normal e instalación en los diferentes entornos. De esta manera garantizamos que el código ha sido probado correctamente y una cierta calidad del mismo, de manera automática. 

Pese a que a todos nos gustaría un mundo ideal en que haya un Coverage del 100% en todos los tipos, hemos de adaptar las condiciones a la realidad. En general, llegar a un Coverage del 80% en desarrollos nuevos ya se considera muy buena métrica. Además, se pueden dar casos en que se estén realizando pruebas para un proyecto ya acabado (por ejemplo, en casos de aplicaciones legacy) y tengamos que partir de métricas mínimas más bajas, como un 30%, e ir subiendo a medida que se desarrollen estas pruebas.  

En general habrá que adaptar las condiciones a la realidad y situación de cada caso. 

Conclusiones

Code Coverage es una métrica muy potente a la hora de garantizar la calidad de los proyectos y automatizar el testeo. Nos permite parar código no testeado antes de que llegue a entornos productivos, y a forzar a pensar en una programación de pruebas en que realmente consultemos todo lo que puede hacer la aplicación, sin dejarnos apartados ni funcionalidades.  

Con Code Coverage podemos mejorar la calidad de las entregas y las pruebas y asegurarnos de que nuevas funcionalidades y desarrollos no se hagan sin un testeo automático exhaustivo que nos dé seguridad y confianza. 

Si tienes preguntas, necesitas asesoramiento o deseas discutir cómo podemos colaborar, no dudes en ponerte en contacto con nosotros.

Daniel Morales Fitó – Cloud Engineer at Itequia