Escena Viva ya tiene batería de pruebas: unitarias del dominio y de la política de permisos, dobles para el reloj y la red, e integración con supertest contra la base de datos real. npm test está verde.

Pero hay dos preguntas sin responder. La primera: ¿qué partes del código no ha ejecutado nunca ninguna prueba? Puede haber una rama del if de puedeCambiarRol que jamás se recorre, un catch que nunca se dispara, una función exportada que nadie llama. Eso lo responde la cobertura. La segunda es más práctica: ¿qué pasa cuando la batería crece? Hoy tarda un segundo; con doscientas pruebas de integración tardará cinco minutos, y una prueba que falla una de cada ocho veces convertirá el trabajo del equipo en una lotería.

Contenido

  1. Qué mide la cobertura y qué no
  2. Instalar y leer c8
  3. Umbrales y exclusiones
  4. Por qué el 100 % no es el objetivo
  5. Dónde está el riesgo en Escena Viva
  6. Organizar los scripts de npm
  7. Pruebas rápidas y fiables
  8. Cazar una prueba intermitente
  9. Preparar el proyecto para integración continua
  10. Ganchos de git

Qué mide la cobertura y qué no

La cobertura mide qué porcentaje del código se ejecutó durante las pruebas, y se descompone en cuatro métricas que se confunden a menudo:

Métrica Qué cuenta Ejemplo
Líneas (lines) Líneas de código ejecutadas ¿Se ejecutó la línea 42?
Sentencias (statements) Sentencias ejecutadas; una línea puede tener varias const a = 1; const b = 2; son dos
Ramas (branches) Caminos de cada decisión: if/else, ?:, &&, ??, case ¿Se probó el if y el camino sin if?
Funciones (functions) Funciones invocadas al menos una vez ¿Se llamó alguna vez a puedeCambiarRol?

La que de verdad importa es la de ramas. Fíjate en este ejemplo, donde el 100 % de líneas convive con la mitad de las ramas sin probar:

function calcularTotal(lineas, descuento) {
  const bruto = lineas.reduce((suma, l) => suma + l.cantidad * l.precioCentimos, 0);
  return descuento ? Math.floor(bruto * 0.9) : bruto;
}

// Una única prueba, sin descuento:
expect(calcularTotal([{ cantidad: 2, precioCentimos: 2500 }], false)).to.equal(5000);

Esa prueba ejecuta todas las líneas: 100 % de líneas, de sentencias y de funciones. Y el 50 % de ramas, porque la del descuento nunca se recorre. Si Math.floor(bruto * 0.9) estuviera mal escrito —pongamos bruto * 0.09—, el informe seguiría luciendo casi perfecto. Lo mismo ocurre en nuestra política de autorización:

function puedeGestionarEvento(usuario, evento) {
  if (!usuario || !evento) return false;
  if (usuario.rol === 'administrador') return true;
  return usuario.rol === 'organizador' && usuario.salaId === evento.salaId;
}

Una sola prueba con un administrador ejecuta las dos primeras líneas y sale: cobertura de líneas alta, cobertura de ramas baja, porque no se ha probado el usuario nulo, ni el organizador de la sala correcta, ni el de la incorrecta, ni el asistente. Y ahí es exactamente donde vive el fallo de seguridad.

Y lo que la cobertura no mide, que es lo más importante de esta lección: no mide si comprobaste algo (ejecutar una línea y afirmar sobre su resultado son cosas distintas), no mide si tus aserciones son correctas (expect(true).to.be.true cubre igual), no mide los casos que faltan (si vender() no comprueba el aforo, la cobertura será del 100 % y la sobreventa existirá) y no dice nada sobre la calidad del diseño. La cobertura es un detector de agujeros, no un certificado de calidad: responde "¿qué me he dejado?", no "¿está bien probado?".

Instalar y leer c8

Usaremos c8, que aprovecha la cobertura nativa de V8: no instrumenta ni transforma el código y funciona con CommonJS y ESM sin configuración. Su alternativa clásica es nyc, heredera de Istanbul, que instrumenta el código antes de ejecutarlo; sigue siendo perfectamente válida y su formato de informe es el mismo, pero c8 es más simple de montar en un proyecto moderno.

npm install --save-dev c8
$ npx c8 mocha

  ... 74 passing (3.1s)

------------------------|---------|----------|---------|---------|-------------------
File                    | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------------------|---------|----------|---------|---------|-------------------
All files               |   81.42 |    72.09 |   84.61 |   81.42 |
 src/autorizacion       |  100.00 |   100.00 |  100.00 |  100.00 |
 src/dominio            |   97.11 |    91.30 |   96.55 |   97.11 |
  gestor-ventas.js      |   93.75 |    83.33 |   90.00 |   93.75 | 48-51
 src/servicios          |   64.28 |    51.72 |   70.00 |   64.28 |
  tokens.js             |   72.41 |    55.00 |   75.00 |   72.41 | 88-97,112
  auditoria.js          |   31.03 |    16.66 |   33.33 |   31.03 | 12-38,45-52
 scripts                |    0.00 |     0.00 |    0.00 |    0.00 |
  semilla.js            |    0.00 |     0.00 |    0.00 |    0.00 | 1-96
------------------------|---------|----------|---------|---------|-------------------

Cómo se lee, de derecha a izquierda. Uncovered Line #s es la columna más útil: son las líneas que ninguna prueba ejecutó, así que ve directo ahí. Un % Branch muy por debajo de % Stmts señala funciones con muchas decisiones y pocos escenarios probados: mira tokens.js, con 72 % de sentencias y 55 % de ramas, lo que traducido significa que probamos el camino feliz de los tokens y casi ninguno de los de error. src/autorizacion al 100 % es el fruto de la tabla de casos de 09-02, y ahí está la lección: las funciones puras se cubren enteras con muy poco esfuerzo. Y scripts/semilla.js al 0 % no es un problema, es un script de utilidad que vamos a excluir.

Para investigar de verdad, el informe HTML (c8 --reporter=html mocha, que genera coverage/index.html) pinta el código fuente coloreado: las líneas no ejecutadas salen en rojo y las ramas parcialmente cubiertas aparecen marcadas con una I (rama if no tomada) o una E (rama else no tomada). Es la forma más rápida de ver que un catch entero nunca se ha disparado. Añade coverage/ a .gitignore: es un artefacto generado, no código.

Umbrales y exclusiones

Un informe que nadie mira no sirve de nada. Los umbrales convierten la cobertura en una condición de fallo: si baja del mínimo, el comando sale con código distinto de cero y CI se pone rojo.

{
  "all": true,
  "include": ["src/**/*.js"],
  "exclude": ["src/config/index.js", "src/servidor.js", "src/db/sequelize.js"],
  "reporter": ["text", "html", "lcov"],
  "lines": 75,
  "branches": 65,
  "functions": 75,
  "check-coverage": true
}

La opción all merece un párrafo. Sin ella, c8 solo informa de los ficheros que las pruebas cargaron: si creas src/servicios/notificaciones.js y no escribes ninguna prueba, el fichero no aparece y la cobertura global no baja. Con all: true aparece con un 0 % rotundo y la media cae, que es exactamente lo que quieres que pase. El resto: include limita la medición a src/, reporter produce texto para la consola, HTML para investigar y lcov para el servidor de CI, y check-coverage hace fallar la ejecución si no se cumplen los mínimos.

Sobre la política de umbrales, dos reglas, y la segunda es la importante. Empiezan bajos: fija el umbral ligeramente por debajo de la cobertura actual, porque un umbral inalcanzable se ignora y un umbral ignorado no existe. Y nunca se bajan: cuando la cobertura suba a 85 %, sube el umbral a 82, y cuando alguien proponga bajarlo porque "esta entrega urgente no da tiempo a probar", la respuesta es no. El umbral es un trinquete que solo gira en un sentido; en el momento en que se puede bajar, deja de ser una restricción y pasa a ser una sugerencia. Una variante útil son los umbrales por carpeta, exigiendo más al código crítico: c8 --include='src/dominio/**' --include='src/autorizacion/**' --lines=95 --branches=90 --check-coverage mocha test/unidad/**/*.test.js.

Excluir ficheros es legítimo cuando medirlos no aporta información, y un fraude cuando se hace para maquillar el número.

Exclusiones legítimas Por qué
migrations/ de sequelize-cli Código generado que se ejecuta una vez; se verifica ejecutándolo
scripts/semilla.js Herramienta de desarrollo, ya ejercitada por las pruebas de integración
src/config/index.js Lectura de variables de entorno; se ejercita al arrancar cualquier prueba
src/servidor.js Llama a listen y escucha señales; probarlo requeriría matar procesos
Ficheros de configuración y el propio test/ Configuración, no lógica

Son ilegítimas, y banderas rojas en una revisión de código: excluir src/controladores/ porque "es difícil de probar" (es donde vive la lógica de la API), añadir /* c8 ignore next */ sobre una rama que sí se puede probar solo para que el umbral pase, excluir el fichero que acaba de causar una incidencia en producción, o excluir carpetas enteras sin comentario que lo justifique. Cuando uses una exclusión en línea, deja escrito el motivo: /* c8 ignore next 3 -- rama defensiva: solo ocurre si el motor cambia el formato de error */.

Por qué el 100 % no es el objetivo

Perseguir el 100 % produce, de forma sistemática, pruebas peores. Este es el mecanismo:

// Prueba escrita para subir la cobertura de auditoria.js del 31 % al 90 %
it('registra en auditoria', async () => {
  await auditoria.registrar('compra', { usuarioId: 'usr-001' });
  // ...y ya. No hay ni una aserción.
});

Esta prueba ejecuta todo el fichero, sube la cobertura veinte puntos y no puede fallar nunca salvo que el código lance. Si registrar guardara el evento con el usuario equivocado, en la colección equivocada o con la fecha del año pasado, seguiría verde. Peor aún: ahora existe una prueba que hay que mantener, que tarda, y que da la falsa impresión de que la auditoría está probada, así que la próxima persona no escribirá la prueba de verdad porque "ya hay una".

Los últimos puntos son además los más caros. Ir del 80 % al 90 % suele ser trabajo útil —ramas de error, casos límite, validaciones—; ir del 95 % al 100 % suele significar inventar escenarios artificiales para disparar catch defensivos que solo ocurrirían si Node se rompiera.

Si la cobertura no dice si tus pruebas son buenas, ¿qué lo dice? Las pruebas de mutación, una idea tan elegante como incómoda: una herramienta modifica automáticamente tu código de producción —cambia un >= por >, un && por ||, borra una línea, invierte un booleano— y vuelve a ejecutar la batería. Si las pruebas fallan, han "matado al mutante": detectan ese fallo. Si pasan con el código mutado, el mutante ha sobrevivido y tienes código ejecutado pero no verificado. El resultado es una puntuación de mutación, que mide algo mucho más cercano a lo que nos importa: no cuánto código se ejecuta, sino cuánto está realmente vigilado. En Node la herramienta de referencia es Stryker Mutator, y su inconveniente es el coste, porque ejecuta la batería una vez por mutante. La práctica razonable es aplicarla solo sobre el código crítico —en Escena Viva, src/dominio/ y src/autorizacion/— y de forma periódica, no en cada commit. La mencionamos porque es el antídoto conceptual contra la fe ciega en el porcentaje.

Dónde está el riesgo en Escena Viva

No todo el código merece la misma cobertura. Este es nuestro reparto razonado:

Capa Objetivo Por qué
src/autorizacion/ 95-100 % Funciones puras, baratísimas de probar, y un fallo es una brecha de seguridad
src/dominio/ 90-95 % Reglas de negocio; un fallo vende entradas que no existen. Sin dependencias
tokens.js, contrasenas.js 85-90 % Seguridad, con muchas ramas de error que hay que forzar con dobles
src/middleware/ 80 % Se ejercita mucho por integración; las ramas raras merecen prueba unitaria
src/controladores/ 70-80 % Cubiertos sobre todo por integración; su lógica propia debería ser poca
src/repositorios/ 70 % Cubiertos por integración con base de datos real
src/rutas/ 60 % Casi declarativos; lo que importa es que respondan, y eso lo mide integración
scripts/, migrations/ Excluidos Herramientas de un solo uso

La lectura de esta tabla es el mensaje central de la lección: la cobertura no se persigue de forma uniforme, se dirige hacia donde duele el fallo. Un 100 % en src/rutas/ no protege de nada; un 70 % en src/autorizacion/ es negligencia.

Organizar los scripts de npm

Con la batería creciendo, un solo npm test no basta:

{
  "pretest": "npm run lint",
  "test": "mocha",
  "test:unidad": "mocha test/unidad/**/*.test.js",
  "test:integracion": "mocha test/integracion/**/*.test.js",
  "test:ver": "mocha --watch --reporter=min test/unidad/**/*.test.js",
  "test:cobertura": "c8 mocha",
  "test:ci": "c8 --check-coverage mocha --forbid-only",
  "comprobar": "npm run lint && npm run test:cobertura"
}

Las decisiones que hay detrás. pretest se ejecuta automáticamente antes de test por convención de npm, así que si el linter falla las pruebas ni arrancan y un error de sintaxis se detecta en un segundo en vez de en treinta. test:unidad y test:integracion van separados porque tienen naturaleza distinta: las unitarias tardan milisegundos y las lanzas cada dos minutos, las de integración necesitan base de datos. test:ver solo ejecuta las unitarias y con el informe min: es el script que dejas abierto en una terminal mientras programas, y meter las de integración lo haría inservible. test:ci es la variante para el servidor, con umbrales y .only prohibido. Y comprobar es el "¿puedo subir esto?" previo a un pull request.

Pruebas rápidas y fiables

Mocha puede repartir los ficheros entre varios procesos con mocha --parallel --jobs 4. En las unitarias es una ganancia limpia, porque no comparten nada. En las de integración contra una base compartida es un desastre, y conviene entender por qué: cada trabajador ejecuta su propio beforeEach con sembrarDatosDePrueba(), que borra las colecciones, así que si el trabajador A borra la base mientras B comprueba el aforo de ses-001-1, B falla por un motivo ajeno a su código y de forma distinta en cada ejecución. Las salidas posibles son no paralelizar la integración (nuestra elección: son pocas y tardan segundos), dar una base por trabajador usando process.env.MOCHA_WORKER_ID en el nombre, o aislar por espacio de nombres para que cada prueba use identificadores propios y no borre nada global.

Un ajuste de velocidad muy rentable y específico de este proyecto: bcrypt con coste 12 tarda unos 300 ms a propósito, y en .env.test puedes poner BCRYPT_COSTE=4. Con dos condiciones: que src/config/index.js lo lea, y que exista una prueba que verifique que en producción el coste es 12; de lo contrario acabas desplegando hashes débiles.

Otras herramientas: mocha --bail se detiene en el primer fallo, útil cuando ya sabes que algo está roto; --sort da un orden alfabético determinista; y --retries 2 reintenta las fallidas, con una advertencia seria. Reintentar oculta las pruebas intermitentes en lugar de arreglarlas, y una prueba intermitente casi siempre indica un fallo real de concurrencia o de estado en tu código, no en la prueba; úsalo, si acaso, solo en extremo a extremo y con un plazo para investigar. Lo contrario sí es muy recomendable: aleatorizar el orden para descubrir dependencias ocultas. Mocha no lo trae de serie, pero ejecutar los ficheros en orden inverso de vez en cuando (mocha $(ls test/unidad/*.test.js | sort -r)) es información valiosísima y gratis: si la batería falla con otro orden, tienes estado compartido.

Cazar una prueba intermitente

Una prueba intermitente (flaky) pasa unas veces y falla otras sin que el código cambie. Es el cáncer de las baterías: erosiona la confianza hasta que el equipo re-ejecuta CI por costumbre, y en ese momento la batería ya no informa de nada.

Causa Síntoma Diagnóstico Solución
Tiempo real Falla al cambiar de día, mes o año; o en máquinas lentas Aparece Date.now() o setTimeout en el código probado Reloj falso de Sinon (09-03)
Orden de ejecución Pasa sola, falla en batería (o al revés) Ejecuta el fichero solo y la batería en orden inverso Estado limpio en beforeEach, nunca en before
Estado compartido Falla la segunda ejecución seguida Variables de módulo, cachés, datos que sobreviven Función de reinicio, sinon.restore(), limpiar la base
Promesas no esperadas Fallo en una prueba posterior, o unhandledRejection suelto Busca it sin async, await olvidados Esperar siempre todo
Puertos o recursos fijos EADDRINUSE; falla solo en CI Un listen(3000) en algún sitio Puerto efímero (supertest ya lo hace)

El método de caza, paso a paso:

# 1. Reproducir: ejecutar la prueba sospechosa cincuenta veces
for i in $(seq 1 50); do
  npx mocha test/integracion/pedidos.test.js --grep "no sobrevende" || echo "FALLO en $i";
done

# 2. ¿Falla sola o solo en batería?  3. ¿Depende del orden?
npx mocha test/integracion/pedidos.test.js
npx mocha $(ls test/**/*.test.js | sort -r)

Y la regla de gestión, tan importante como la técnica: una prueba intermitente se arregla o se borra, nunca se ignora. Si no puedes arreglarla ahora, márcala con .skip y un comentario con el motivo y la fecha. Una prueba desactivada y documentada es honesta; una que falla al azar es ruido que enseña al equipo a ignorar el color rojo.

Preparar el proyecto para integración continua

En el módulo 11 montaremos el flujo completo de GitHub Actions. Aquí dejamos el proyecto en condiciones de que cualquier servidor pueda ejecutar las pruebas sin intervención humana. Los requisitos son seis.

1. Instalación reproducible. En CI se usa npm ci, no npm install: instala exactamente lo que dice package-lock.json, borrando node_modules antes. Esto exige que el lock esté versionado y actualizado.

2. Configuración por variables de entorno, no por ficheros locales. .env.test está en .gitignore, así que en CI las variables se inyectan; por eso el módulo 6 dejó src/config/index.js como único punto de lectura de process.env, y cambiar de entorno no toca el código. Documenta las variables necesarias en un .env.test.example que sí se versiona, con NODE_ENV, las URI de las bases con sufijo _test, JWT_SECRETO=cambiar-en-cada-entorno, BCRYPT_COSTE=4, NIVEL_REGISTRO=silent y TZ=UTC, sin secretos reales.

3. Base de datos efímera. El servidor levanta Mongo y PostgreSQL como servicios del trabajo, vacíos, y nuestra semilla en beforeEach los deja listos. El requisito es que la batería no asuma datos preexistentes: si alguna prueba depende de un dato que insertaste a mano en tu máquina, fallará en CI y no en local, que es la peor combinación posible.

4. Salida con código distinto de cero, que es lo que un servidor de CI entiende como fallo. mocha sale con el número de pruebas fallidas y c8 --check-coverage sale con 1 si no se cumple el umbral; compruébalo con npm run test:ci; echo "codigo de salida: $?".

5. Sin interacción y sin procesos que queden vivos. Nada de prompt ni de esperar una tecla; y con "exit": false en .mocharc.json, una conexión sin cerrar cuelga el trabajo hasta que el servidor lo mate por tiempo. Por eso el afterAll con desconectar() de la lección anterior no es cosmética.

6. Determinismo horario. Los servidores de CI corren en UTC, así que si alguna prueba asume tu zona horaria fallará allí: fija TZ=UTC en .env.test y trabaja con fechas ISO, como venimos haciendo.

Con esto, el flujo del módulo 11 se reduce a npm ci, levantar los servicios y npm run test:ci. El trabajo duro es este, no el YAML.

Ganchos de git

Los umbrales y los scripts solo sirven si alguien los ejecuta. Un gancho de git ejecuta un comando antes de completar una operación, de modo que la comprobación deja de depender de la disciplina de cada persona; el más útil es pre-commit, que puede impedir un commit si algo falla.

npm install --save-dev husky lint-staged
npx husky init          # crea .husky/pre-commit

En .husky/pre-commit ponemos npx lint-staged seguido de npm run test:unidad, y en package.json la configuración de qué hacer con cada tipo de fichero:

{
  "lint-staged": {
    "*.js": ["eslint --fix", "prettier --write"],
    "*.{json,md}": ["prettier --write"]
  }
}

lint-staged es la clave del equilibrio: solo procesa los ficheros en el área de preparación, no el proyecto entero. Corregir el formato de tres ficheros tarda medio segundo; hacerlo de doscientos, quince.

Momento Qué se ejecuta Presupuesto de tiempo
pre-commit lint-staged + pruebas unitarias Menos de 10 segundos
pre-push Batería completa con integración Menos de 2 minutos
CI (módulo 11) Todo, más cobertura y auditoría de dependencias Lo que haga falta

Y la advertencia: no conviertas cada commit en una espera de cinco minutos. Si el gancho es lento, la gente aprende a escribir git commit --no-verify y el gancho deja de existir. Un gancho rápido que se ejecuta siempre protege infinitamente más que uno exhaustivo que todo el mundo esquiva.

Esa idea merece cerrarse con la consecuencia menos técnica y más determinante del módulo: una batería lenta no falla, se abandona, y la secuencia es siempre la misma. La batería tarda ocho minutos y la gente deja de ejecutarla en local, confiando en CI. Como el ciclo de realimentación pasa de segundos a minutos, se hacen commits más grandes y menos frecuentes. Cuando CI se pone rojo, el cambio ya es enorme y localizar la causa cuesta. Aparece una prueba intermitente y, como re-ejecutar cuesta ocho minutos, alguien añade --retries. Un fallo real se esconde detrás de un reintento y llega a producción. Conclusión del equipo: "las pruebas no sirven de nada". Y con esa batería, es verdad. Por eso la pirámide tiene la forma que tiene, por eso separamos test:unidad de test:integracion y por eso bajamos el coste de bcrypt en pruebas: una batería de dos segundos se ejecuta cien veces al día; una de dos minutos, dos veces.

Errores Comunes y Consejos

  • Confundir cobertura con calidad. Un 95 % con pruebas sin aserciones protege menos que un 70 % con pruebas exigentes.
  • No activar all: true. Los ficheros sin ninguna prueba desaparecen del informe y la media miente descaradamente.
  • Bajar un umbral para que pase la entrega. El trinquete se rompe una sola vez y ya no vuelve a subir.
  • Excluir lo difícil de probar. Es justo lo que hay que probar; la dificultad suele venir del acoplamiento (09-03).
  • Paralelizar la integración con base compartida. Fallos aleatorios imposibles de reproducir.
  • Abusar de --retries. Convierte un fallo real de concurrencia en ruido tolerado.
  • Ganchos de pre-commit lentos. Se acaban esquivando con --no-verify.
  • Consejo: cuando la cobertura de ramas de un fichero sea mucho menor que la de líneas, abre su informe HTML; las ramas rojas suelen ser exactamente los catch y los casos de error sin probar.
  • Consejo: revisa la cobertura de lo que cambia en cada pull request, no la global. La media del proyecto se mueve muy despacio y oculta que el módulo nuevo entró con un 20 %.

Ejercicios

Ejercicio 1: encontrar la rama sin probar

Ejecuta npm run test:cobertura y abre el informe HTML. Localiza la función de src/servicios/tokens.js con menor cobertura de ramas, identifica qué escenario falta y escribe la prueba que lo cubra. Pista: casi siempre es un camino de error que requiere un doble de Sinon.

Ejercicio 2: el umbral trinquete

Configura .c8rc.json con los umbrales actuales menos tres puntos. Después escribe tres pruebas nuevas para src/dominio/gestor-ventas.js que cubran las líneas 48-51 del informe de ejemplo, vuelve a medir y sube los umbrales al nuevo nivel menos tres. Documenta la fecha y el valor para que el trinquete sea visible.

Ejercicio 3: fabricar y cazar una intermitente

Escribe a propósito una prueba intermitente: una que compruebe que el código de entrada del pedido creado empieza por EV-2026- usando el año real del sistema. Después explica en qué momento fallará, demuéstralo con un reloj falso situado en 2027-01-01 y arréglala de forma que no dependa del calendario.

Soluciones

Ejercicio 1. La función con peor cobertura de ramas suele ser rotarRefresco, porque el camino feliz se prueba y los de error no. El escenario que falta es la reutilización de un token ya rotado:

it('revoca la familia entera si se presenta un refresco ya rotado', async () => {
  const repositorio = {
    buscarToken: sinon.stub().resolves({ familia: 'fam-001', usadoEn: '2026-10-01T10:00:00.000Z' }),
    revocarFamilia: sinon.stub().resolves(),
  };

  let capturado = null;
  try {
    await rotarRefresco('token-antiguo', { repositorio });
    expect.fail('Se esperaba ErrorDeAutenticacion por reutilizacion');
  } catch (error) { capturado = error; }

  expect(capturado.codigo).to.equal('REFRESCO_REUTILIZADO');
  expect(repositorio.revocarFamilia.calledOnceWith('fam-001')).to.be.true;
});

La aserción sobre revocarFamilia es la esencial: no basta con rechazar la petición, hay que invalidar toda la cadena porque el token pudo ser robado.

Ejercicio 2. El .c8rc.json queda con "lines": 78, "branches": 68, "functions": 78 si la cobertura actual es 81/72/84. Las tres pruebas para las líneas 48-51 de gestor-ventas.js —el camino que no emite aforo-bajo cuando la sesión ya estaba por debajo del umbral, el que ignora ventas de cantidad cero y el de la sesión ya agotada— son exactamente el tipo de rama defensiva que la cobertura saca a la luz. Tras cubrirlas, el nuevo valor podría ser 84/76/85 y los umbrales pasarían a 81/73/82. Y el trinquete se documenta en el README, no en la memoria de nadie.

Ejercicio 3. Fallará el 1 de enero de 2027 a las 00:00 sin que nadie haya tocado el código, y también en un servidor de CI con la zona horaria desplazada si la ejecución cae justo en el cambio de año. La demostración consiste en envolver la compra con sinon.useFakeTimers(new Date('2027-01-01T00:00:01.000Z')) y comprobar que la aserción to.match(/^EV-2026-/) falla porque llega EV-2027-. El arreglo tiene dos variantes válidas:

// Variante 1: afirmar el FORMATO, no el año concreto
expect(body.entradas[0].codigo).to.match(/^EV-\d{4}-\d{6}$/);

// Variante 2: si el año importa, fijarlo con el reloj falso
const reloj = sinon.useFakeTimers(new Date('2026-11-05T12:00:00.000Z'));
// ...la compra, y después
expect(body.entradas[0].codigo).to.match(/^EV-2026-/);
reloj.restore();

La lección general: una prueba nunca debe depender de un dato que cambia solo. O afirmas la propiedad estable (el formato), o controlas la fuente de variabilidad (el reloj).

Conclusión

Ya no solo tenemos pruebas: sabemos qué protegen y qué no. Entendemos las cuatro métricas de cobertura y por qué la de ramas es la que importa, tenemos c8 conectado con informe de texto y HTML, umbrales en .c8rc.json que funcionan como trinquete, exclusiones justificadas y un reparto de objetivos por capa que dirige el esfuerzo hacia donde el fallo duele: la política de autorización y el dominio, no las rutas.

Sabemos también que el 100 % es una trampa, que una prueba sin aserciones sube la métrica sin proteger nada, y que la respuesta seria a "¿son buenas mis pruebas?" son las pruebas de mutación. Tenemos los scripts de npm organizados con pretest, test:ver para el ciclo corto y test:ci para el servidor. Sabemos cazar una prueba intermitente con un método, no con reintentos. Y el proyecto está listo para que un servidor de integración continua lo ejecute solo: npm ci, variables de entorno, base efímera y código de salida distinto de cero. El flujo de GitHub Actions completo nos espera en el módulo 11.

Queda una última pieza. Las pruebas te dicen que algo está roto; casi nunca por qué. Cuando una prueba de integración devuelve 500 en lugar de 201, cuando el proceso no termina y no sabes qué recurso quedó abierto, cuando la memoria del servidor crece sin parar durante la semana, hace falta otra herramienta.

En la lección siguiente, Depuración de Aplicaciones Node.js, cerramos el módulo: del console.log bien usado al inspector de V8, puntos de interrupción condicionales, depurar las propias pruebas de Mocha desde VS Code, leer trazas asíncronas, diagnosticar EADDRINUSE, E11000, unhandledRejection y el proceso que no muere, encontrar una fuga de memoria comparando instantáneas del montículo, y un método de depuración que termina donde ha empezado este módulo: escribiendo una prueba que reproduzca el fallo antes de arreglarlo.

Curso de Node.js: De Principiante a Avanzado

Módulo 1: Introducción a Node.js

Módulo 2: Conceptos Básicos

Módulo 3: Sistema de Archivos y E/S

Módulo 4: HTTP y Servidores Web

Módulo 5: NPM y Gestión de Paquetes

Módulo 6: Framework Express.js

Módulo 7: Bases de Datos y ORMs

Módulo 8: Autenticación y Autorización

Módulo 9: Pruebas y Depuración

Módulo 10: Temas Avanzados

Módulo 11: Despliegue y DevOps

Módulo 12: Proyectos del Mundo Real

© Copyright 2026. Todos los derechos reservados