@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
| Antes | Después | |
|---|---|---|
| Cobertura de la librería | 98,6 % (declarada) / 58,4 % (real) | 98,76 % (medida) |
| Cobertura de la app | 64 % | 85,11 % |
| Pruebas | 308 | 1111 |
| 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.
