Cuando la cobertura miente

@nestjslatam/ddd-lib reportaba 98,6 % de cobertura. La cifra real era 58,4 %, y en un módulo concreto, 8,5 %.

Ninguna de las dos cifras era mentira. Ambas salían de Jest, correctamente calculadas. La diferencia estaba en qué se le pidió medir.

1. El patrón excluía lo no probado

\"collectCoverageFrom\": [\"src/**/*.ts\", \"!src/**/*.spec.ts\"]

Se ve inofensivo. El problema aparece cuando alguien añade un directorio fuera de src/. Los ficheros de libs/ddd/src/ no encajaban con el patrón, así que no salían en el informe. No como 0 %: no aparecían.

Un fichero sin cubrir baja la media. Un fichero ausente, no.

2. Los globs no siguen ../

\"collectCoverageFrom\": [\"../libs/ddd/src/**/*.ts\"]

No funciona, y sin decirlo. collectCoverageFrom resuelve contra rootDir y no atraviesa ../. El patrón no casa con nada, Jest no avisa de que un patrón no casó con nada, y el informe sale idéntico.

3. Clave de directorio vs. clave de glob

\"coverageThreshold\": {
  \"global\": { \"lines\": 80 },
  \"./src/valueobjects/\":        { \"lines\": 95 },  // AGREGA el directorio
  \"./src/valueobjects/**/*.ts\": { \"lines\": 95 }   // se aplica a CADA fichero
}

La primera forma permite que un fichero al 20 % se esconda detrás de nueve al 99 %. La segunda no.

Y hay un efecto de segundo orden que casi nadie conoce: los ficheros que casan con una clave de ruta salen del cómputo de global. Añadir un umbral estricto a un directorio bien probado sube el global de los demás, porque quita del promedio a los buenos. El número mejora justo cuando empeoras la vigilancia.

4. La puerta de CI que se saltaba a sí misma

COVERAGE=$(node -p \"require(\'./coverage/coverage-summary.json\').total.lines.pct\")
if [ \"$COVERAGE\" -lt \"$COVERAGE_THRESHOLD\" ]; then exit 1; fi

Si el fichero no existe, node -p escribe en stderr, COVERAGE queda vacío, y [ \"\" -lt \"80\" ] en bash no es un error de sintaxis: devuelve falso. La puerta pasa.

Un guardián que, cuando no encuentra la puerta, decide que está cerrada.

5. El umbral que cambió de significado dos veces

COVERAGE_THRESHOLD empezó valiendo 80 y refiriéndose a la librería. Se movió a un workflow donde medía la aplicación de ejemplo. Después a otro que medía ambas juntas. El nombre nunca cambió y el valor tampoco. El mismo 80 significaba «exigente», «laxo» y «trivial» según el mes.

Cómo se ve la verdad

npx jest --coverage --collectCoverageFrom=\'**/*.ts\' --coverageReporters=text

# y la comprobación que realmente cierra el asunto:
node -p \"Object.keys(require(\'./coverage/coverage-final.json\')).length\"

Si esperabas 120 ficheros y salen 74, ya sabes por dónde empezar. El fallo casi nunca es un porcentaje bajo: es un fichero ausente.

Y un sexto, de otro tipo

La capa de aplicación marcaba 0 % y llevábamos tiempo tratándola como deuda. No lo era: diecisiete pruebas e2e la recorrían entera, pero escribían su informe en otro directorio y nadie los sumaba.

const libCoverage = require(\'istanbul-lib-coverage\');
const map = libCoverage.createCoverageMap({});
map.merge(require(\'./coverage/coverage-final.json\'));
map.merge(require(\'./coverage-e2e/coverage-final.json\'));

istanbul-lib-coverage viene con Jest; no hay dependencia nueva. La cobertura real pasó de un 64 % aparente a un 85,11 % medido. Un fichero está cubierto si un test lo recorre; cuál de los dos ejecutores lo hizo es un accidente de cómo repartiste las suites.

El resultado

AntesDespués
Cobertura de la librería98,6 % (declarada) / 58,4 % (real)98,76 % (medida)
Cobertura de la app64 %85,11 %
Pruebas3081111
Bugs encontrados al subirla—34 confirmados

Esos 34 bugs llevaban ahí todo el tiempo. Lo único que cambió fue que alguien miró.

El número que te tranquiliza suele ser el que no está midiendo lo que crees. Una cobertura alta responde a «de lo que decidí medir, ¿cuánto se ejecuta?». Nunca responde «¿decidí medir lo correcto?». Y esa segunda pregunta no te la va a hacer ninguna herramienta.

Leave a Comment