Entendiendo Code coverage usando Jest

Entendiendo Code coverage usando Jest

Code Coverage es la métrica utilizada para conocer en qué porcentaje se ha analizado el código a través de las pruebas unitarias. En este artículo, explicaré como interpretar estas métricas y también como mejorarlas, mi ejemplo esta basado en una aplicación de NodeJS para la cual escribiremos un par de pruebas para ver los resultados.

Asumiré que si has llegado aquí es porque ya tienes conocimiento sobre Jest y aunque Istanbul es una librería separada a Jest, internamente Jest integra Istanbul y se puede activar fácilmente utilizando en jest el flag --coverage.

Activando Jest desde el package.json

Para este artículo he creado una aplicación en NodeJS, en la cual he instalado Jest y he habilitado Code Coverage desde el package.json de la siguiente manera:


{
  "name": "debugging-test",
  "version": "1.0.0",
  "main": "./src/index.js",
  "license": "MIT",
  "scripts": {
    "test": "jest --coverage"
  },
  "dependencies": {
    "jest": "^26.6.3"
  }
}

Como convención, he creado un folder llamado __tests__ en la raíz de mi proyecto y la idea es poner ahí dentro todos los archivos de pruebas. Para este proyecto, tengo la siguiente estructura de carpetas:

.
└── debugging-test/
    ├── __tests__
    ├── node_modules
    ├── src/
    │   ├── libs/
    │   │   └── isImage.js
    │   └── index.js
    ├── package.json
    └── yarn.lock

Escribiendo las pruebas

Lo que pretendo hacer es ejecutar una serie de pruebas sobre el archivo isImage.js y para ello, dentro de la carpeta __tests__ he creado el archivo isImage.spec.js con el siguiente código:


const isImage = require("../src/libs/isImage");

describe("Test isImage function", () => {
    it("should return true if a filename is an image", () => {
        const png = isImage("picture.png");
        expect(png).toBe(true);
    });
});

Y al ejecutar las pruebas tengo una salida como la siguiente:


> $ yarn test
yarn run v1.22.0
$ jest --coverage
 PASS  __tests__/isImage.spec.js
  Test isImage function
    ✓ it should return true if a filename is an image (4 ms)

------------|---------|----------|---------|---------|-------------------
File        | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------|---------|----------|---------|---------|-------------------
All files   |   85.71 |       50 |     100 |   85.71 |
 isImage.js |   85.71 |       50 |     100 |   85.71 | 11
------------|---------|----------|---------|---------|-------------------
Jest: "/Users/jack/Public/debugging-test/src/libs/isImage.js" coverage threshold for lines (100%) not met: 85.71%
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.125 s
Ran all test suites.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

Este es el informe generado con las pruebas del archivo isImage.spec.js. Y este Informe dice que del 100% del umbral de cobertura esperado a cubrirse, solamente el 85.71% fue satisfactoriamente probado y revisado, aparte de que se alcanzó a verificar el 85.71% para statements (Stmts), 50% de Branches, 100% de funciones (Funcs) y 85.71% de líneas funcionales (Lines) y que se quedaron 11 líneas de código sin revisar.

Y para entender mejor que significa cada una de las columnas:

  • Stmts Se refiere a todas las declaraciones del programa
  • Branch Se refiere a cada rama (también denominada ruta DD) de cada estructura de control (como en las declaraciones if y case), por ejemplo, dada una declaración if, ¿se han ejecutado las ramas verdadera y falsa? Otra forma de decir esto es, ¿se han ejecutado todos los bordes del programa?
  • Funcs Se refiere al llamado de las funciones o subrutinas del programa
  • Lines Se refiere a cada línea ejecutable en el archivo fuente

Para cada caso, el porcentaje representa el código ejecutado frente al código no ejecutado, lo que equivale a cada fracción en formato de porcentaje (por ejemplo: 50% de ramas, 1/2).

Ahora que tenemos algo de teoría en mente, vean que no es lo mismo haber completado la prueba satisfactoriamente vs haberla completado y ejecutado todos los casos, específicamente en mi prueba, 11 líneas no fueron evaluadas y si reviso el informe, éste dice que solamente el 50% de los branches fueron revisados.

Pero en sí, qué significa que solo el 50% de los branches fueron revisados? para ello veamos el código del archivo isImage.spec.js y les explico que ocurre.


const path = require('path');

function isImage(filepath) {
    const imageTypes = ['.png', '.jpg', '.jpeg'];

    const filetype = path.extname(filepath);

    if (imageTypes.includes(filetype)) {
        return true;
    } else {
        return false;
    }
}

module.exports = isImage;

A simple vista parece estar todo bien, incluso el código podría reducirse un poco más, pero lo que no esta bien es la prueba en sí, ya que solamente estoy evaluando que la función isImage retorne un valor booleano verdadero (true) cuando el código también tiene una condición la cual puede retornar false y las 11 líneas que no se probaron se refiere explícitamente a la función completa pero retornando un valor false.

El fix de esto, sencillo, será necesario agregar el código para evaluar cuando la función retorna false, osea enviar un nombre archivo que no tenga el formato aceptado, entonces volvamos al archivo isImage.spec.js y agregemos la nueva prueba.


const isImage = require("../src/libs/isImage");

describe("Test isImage function", () => {
    it("should return true if a filename is an image", () => {
        const png = isImage("picture.png");
        expect(png).toBe(true);
    });

    it("should return false if a filename is not an image", () => {
        const png = isImage("readme.md");
        expect(png).toBe(false);
    });
});

Y si volvemos a ejecutar las pruebas, esta vez veremos que nuestro archivo se ha ejecutado en un 100% y muestra la siguiente salida:


> $ yarn test
yarn run v1.22.0
$ jest --coverage
 PASS  __tests__/isImage.spec.js
  Test isImage function
    ✓ it should return true if a filename is an image (3 ms)
    ✓ it should return false if a filename is not an image (1 ms)

------------|---------|----------|---------|---------|-------------------
File        | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------|---------|----------|---------|---------|-------------------
All files   |     100 |      100 |     100 |     100 |
 isImage.js |     100 |      100 |     100 |     100 |
------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        1.928 s
Ran all test suites.
✨  Done in 3.26s.

Sé que quizás viendo la tabla en la consola pueda que no sea suficientemente intiutito saber que esta mal o que hizo falta evaluar, por lo que también se pueden generar los informes en formato HTML utilizando el parámetro --coverageDirectory='coverage'.

Yo particularmente utilizo --coverageDirectory='coverage' y --collectCoverageFrom='folder/name/**/*.js' que significan:

  • coverageDirectory: Permitirá la generación de un reporte en formato HTML con navegación de código y explicación de líneas evaluadas.
  • collectCoverageFrom: Jest calculará el code coverage para todos los archivos que se especifique en un folder, incluso aquellos sin pruebas (esto afectará directamente al porcentaje global de líneas revisadas).

Las trampas de apuntar a una cobertura del 100%

Personalmente creo que para proyectos grandes, a medida que se aumenta la cobertura del código, a veces será demasiado difícil cubrir ciertas líneas de código con pruebas unitarias y pasar tiempo tratando de encontrar una solución para cubrir esa línea de código no siempre vale la pena, por lo que no estoy totalmente de acuerdo en cumplir el 100% de las pruebas.

Pensando en este caso, se puede configurar jest para que se pueda alcanzar un umbral menor al 100% y se hace desde el archivo package.json en la sección de jest con la propiedad coverageThreshold, la cual permite definir los umbrales de cobertura para cada criterio.

Por ejemplo, se podría reducir entonces el porcentaje de cobertura para los Branches y líneas no evaluadas con escribir lo siguiente:


{
  "name": "debugging-test",
  "version": "1.0.0",
  "main": "./src/index.js",
  "license": "MIT",
  "scripts": {
    "test": "jest --coverage"
  },
  "dependencies": {
    "jest": "^26.6.3"
  },
  "jest": {
    "coverageThreshold": {
      "global": {
        "branches": 50,
        "functions": 85,
        "lines": 85,
        "statements": 85
      },
      "./src/**/*.js": {
        "lines": 85
      }
    }
  }
}

Aunque claro, esto podría ser considerado trampa, así es que también utilicen estas configuraciones sabiamente, la idea es cubrir la mayor cantidad posible de código con las pruebas.

Espero que el ejempo y la explicación les sirva y ya saben que siempre cualquier comentario es bienvenido.

Happy coding!


Photo by David Travis on Unsplash

Jack Fiallos

Jack Fiallos

Te gustó este artículo?

∅ 5 out of 1 Votes