Las pruebas te dicen que algo está roto. Casi nunca te dicen por qué. Una prueba de integración que esperaba 201 y recibió 500 te ha hecho un favor enorme —ha detectado el fallo antes que un cliente—, pero ahora hay que averiguar qué ocurrió dentro de esa petición: en qué middleware, con qué datos, en qué línea.
Esta lección cierra el módulo con la otra mitad del oficio: las herramientas y el método para diagnosticar. Una frontera antes de empezar, para que no haya confusión: aquí diagnosticamos lo que falla. Medir y optimizar lo que funciona pero va lento —perfiles de CPU, flamegraphs, pruebas de carga— es el módulo 10.
Contenido
- Del
console.logal depurador - El depurador integrado:
--inspect - Depurar desde VS Code, incluidas las pruebas de Mocha
- Puntos de interrupción y ejecución paso a paso
- Depurar código asíncrono
- Leer una traza de error de verdad
- Errores frecuentes y su diagnóstico
- Fugas de memoria
- Depurar en producción sin parar el servicio
- Método de depuración
Del console.log al depurador
Empecemos por lo obvio: console.log no es vergonzoso. Es instantáneo, funciona en cualquier entorno, no requiere configuración y muchas veces resuelve el problema en treinta segundos. Lo que sí es cierto es que obliga a modificar el código, a reiniciar, a adivinar de antemano qué imprimir, y con objetos anidados imprime [Object] justo donde estaba la respuesta. Aprovecha primero todo lo que la consola ofrece más allá de log:
const util = require('node:util');
console.table(evento.sesiones.map((s) => ({ // comparativas legibles de un vistazo
id: s.id, aforo: s.aforo, libres: s.libres,
ocupacion: `${(s.ocupacion * 100).toFixed(1)} %`,
})));
console.dir(pedido, { depth: null, colors: true }); // el remedio contra [Object]
registro.debug(util.inspect(pedido, { depth: 4, breakLength: 120 })); // igual, como texto
console.time('compra'); // cuánto tarda un tramo
await comprarEntradas(datos);
console.timeEnd('compra'); // compra: 187.42ms
console.trace('quien esta llamando a vender()'); // cómo se llegó aquí, sin lanzar
console.count('venta-registrada'); // cuántas veces pasa por aquí
console.assert(sesion.vendidas <= sesion.aforo, 'SOBREVENTA', sesion.id);console.dir(objeto, { depth: null }) es probablemente el truco más rentable de la lista: por defecto Node solo imprime dos niveles de anidamiento, y con un pedido que contiene líneas que contienen entradas esos dos niveles se agotan enseguida.
Aun así, el depurador da tres cosas que la consola no puede dar:
console.log |
Depurador | |
|---|---|---|
| Decidir qué mirar | Antes de ejecutar | Durante la ejecución |
| Ver el ámbito | Solo lo que imprimiste | Todas las variables vivas |
| Pila de llamadas | console.trace |
Navegable, con el ámbito de cada marco |
| Modificar valores | No | Sí, desde la consola en vivo |
| Coste en producción | Contamina los registros | Cero si no está activo |
El depurador integrado: --inspect
Node lleva un depurador incorporado que habla el protocolo de Chrome DevTools, y se activa con dos banderas:
node --inspect src/servidor.js # arranca y activa el inspector
node --inspect-brk src/servidor.js # arranca y SE DETIENE en la primera línea
# Debugger listening on ws://127.0.0.1:9229/8f2a1c3d-...La diferencia es decisiva. --inspect sirve para depurar algo que ocurre más tarde —una petición HTTP concreta, un evento—: el servidor arranca y espera. --inspect-brk sirve para depurar el arranque: la lectura de la configuración, la conexión a la base de datos, el montaje de los middlewares; sin -brk, todo eso ya ha pasado antes de que te dé tiempo a conectarte.
Para conectarte desde Chrome o Edge, abre chrome://inspect, busca tu proceso en "Remote Target" y pulsa inspect: se abre DevTools con la pestaña Sources, donde puedes poner puntos de interrupción, y con la pestaña Memory, que usaremos para las fugas. Existe también un depurador de línea de comandos, node inspect src/servidor.js, útil por SSH donde no hay navegador: c continúa, n avanza una línea, s entra en la función, o sale, repl evalúa expresiones. Es austero, pero un día en un servidor remoto te salvará la tarde.
Depurar desde VS Code, incluidas las pruebas de Mocha
Esta es la forma más productiva en el día a día. En .vscode/launch.json:
{
"version": "0.2.0",
"configurations": [
{
"name": "Servidor Escena Viva",
"type": "node", "request": "launch",
"program": "${workspaceFolder}/src/servidor.js",
"envFile": "${workspaceFolder}/.env",
"skipFiles": ["<node_internals>/**", "${workspaceFolder}/node_modules/**"],
"console": "integratedTerminal"
},
{
"name": "Mocha: el fichero abierto",
"type": "node", "request": "launch",
"program": "${workspaceFolder}/node_modules/mocha/bin/mocha.js",
"args": ["${relativeFile}", "--timeout", "0"],
"envFile": "${workspaceFolder}/.env.test",
"skipFiles": ["<node_internals>/**", "${workspaceFolder}/node_modules/**"],
"console": "integratedTerminal"
},
{
"name": "Adjuntar a proceso existente",
"type": "node", "request": "attach", "port": 9229, "restart": true,
"skipFiles": ["<node_internals>/**"]
}
]
}La segunda configuración —abres pedidos.test.js, pones un punto de interrupción y pulsas F5— es la más usada de todas; basta duplicarla sin ${relativeFile} para depurar la batería completa. La tercera sirve cuando el proceso ya corre con --inspect, por ejemplo dentro de un contenedor.
Tres detalles marcan la diferencia. "--timeout", "0" es imprescindible en las configuraciones de Mocha: sin él, Mocha aborta la prueba a los 5000 ms mientras tú estás parado leyendo variables. skipFiles con node_modules y <node_internals> evita que el paso a paso te meta dentro de Express o de Mongoose, de modo que al "entrar en la función" entres en tu código. Y envFile apuntando a .env.test garantiza que depuras contra la base de pruebas y no contra la de desarrollo.
Depurar las pruebas es, con diferencia, el caso más útil: tienes un fallo reproducible de forma determinista, aislado, con datos conocidos, y puedes pararlo donde quieras. Es la situación ideal para depurar, y es un regalo que te ha hecho el resto del módulo.
Puntos de interrupción y ejecución paso a paso
Hay tres tipos de punto de interrupción, y solo se usa el primero. El normal para siempre en esa línea. El condicional para solo si se cumple una expresión, y es la herramienta que convierte una tarde en cinco minutos: con la condición sesion.id === 'ses-001-1' && cantidad > 4, en un bucle sobre 7 sesiones y 1811 ventas paras exactamente en el caso que investigas (sus variantes son el contador de aciertos, que para en la llamada número 50, y la expresión de registro). El de registro (logpoint) no detiene nada: imprime un mensaje con interpolación, Vendiendo {cantidad} de {sesion.id}, quedan {sesion.libres}, cada vez que pasa por ahí. Es un console.log que no toca el código fuente, perfecto cuando el problema solo aparece bajo concurrencia y detener la ejecución lo haría desaparecer.
Los controles son continuar (F5, sigue hasta el próximo punto), paso por encima (F10, ejecuta la línea sin entrar en las funciones que llame), paso dentro (F11) y paso fuera (Shift+F11, termina la función actual y vuelve al llamante).
Y los cuatro paneles: Variables muestra el ámbito local, el de cierre y el global en el punto donde estás parado, y es donde descubres que req.body es undefined; Inspección sigue expresiones concretas en cada paso (sesion.libres, req.usuario.rol); Pila de llamadas reconstruye cómo se llegó hasta aquí y puedes hacer clic en cualquier marco para ver sus variables, que suele ser donde está el fallo real; y la Consola de depuración evalúa cualquier expresión en el contexto actual.
// Escrito en la consola de depuración, con la ejecución detenida:
sesion.libres // 2
sesion.libres >= req.body.cantidad // false <-- aquí está el 409
req.usuario.rol = 'administrador' // cambiar en vivo y continuar para ver qué pasaEsa última línea es una capacidad enorme: comprobar una hipótesis sin editar, guardar y reiniciar.
Depurar código asíncrono
En Node, la mitad de los problemas ocurren después de un await, y ahí la depuración tiene una peculiaridad. La pila se rompe porque cuando registras un callback con setTimeout o resuelves una promesa, la función se ejecuta más tarde, en otro turno del bucle de eventos, y para entonces la pila original ya se desmontó. Por eso una traza clásica muestra tres marcos (Timeout._onTimeout, listOnTimeout, processTimers), ninguno útil: no dice quién programó ese timeout.
Las trazas asíncronas son la solución. Node mantiene información que permite reconstruir la cadena lógica a través de los await, y DevTools y VS Code la muestran con separadores:
Error: Aforo insuficiente
at Sesion.vender (/app/src/dominio/sesion.js:58:11)
at comprarEntradas (/app/src/servicios/pedidos.js:34:19)
--- await ---
at crearPedido (/app/src/controladores/pedidos.js:22:24)
--- await ---
at Layer.handle (/app/node_modules/express/lib/router/layer.js:95:5)Los marcadores --- await --- separan los tramos asíncronos y se leen de arriba abajo como pasado hacia atrás: el error se lanzó en vender, llamado desde comprarEntradas, que fue esperado desde crearPedido.
Cuatro consejos prácticos. async/await produce trazas mucho mejores que los callbacks anidados, una razón más para preferirlo. Nunca uses catch vacíos: borran la única pista que tenías. Preserva la causa al reenvolver un error, que es lo que hace útil una jerarquía como la nuestra. Y captura los rechazos no gestionados en el arranque para que nunca se pierdan en silencio:
try {
await repositorioPedidos.guardar(pedido);
} catch (error) {
// La opción cause encadena la traza original: sin ella, la pierdes
throw new ErrorDeAplicacion('No se pudo guardar el pedido', {
codigo: 'ERROR_PERSISTENCIA', cause: error,
});
}
// src/servidor.js
process.on('unhandledRejection', (razon) => {
console.error('Promesa rechazada sin gestionar:', razon);
process.exit(1); // fallar rápido: un estado desconocido no es seguro
});Leer una traza de error de verdad
Una traza real está llena de ruido. Vamos a diseccionar una:
ValidationError: Pedido validation failed: lineas.0.cantidad: Path `cantidad` (7)
is more than maximum allowed value (6).
at model.Document.invalidate (/app/node_modules/mongoose/lib/document.js:3241:32)
at /app/node_modules/mongoose/lib/schemaType.js:1368:9
at process.processTicksAndRejections (node:internal/process/task_queues.js:77:11)
at async repositorioPedidos.crear (/app/src/repositorios/pedidos.js:47:20)
at async comprarEntradas (/app/src/servicios/pedidos.js:61:19)
at async crearPedido (/app/src/controladores/pedidos.js:22:24)Se lee en este orden. El tipo y el mensaje primero: un ValidationError de Mongoose con el campo exacto (lineas.0.cantidad), el valor recibido (7) y la regla violada (máximo 6); el 80 % de las veces la primera línea ya lo dice todo. Después salta el ruido de node_modules y node:internal, porque no vas a arreglar Mongoose. Luego busca el primer marco de /app/src/ contando desde arriba —repositorios/pedidos.js:47—, que es donde tu código provocó el error, y sigue bajando por tus marcos para reconstruir el camino: repositorio ← servicio ← controlador. Diagnóstico: alguien pidió 7 entradas y la validación de zod no lo detuvo antes de llegar al modelo, así que el fallo real no es de Mongoose sino un agujero en la validación de entrada, y probablemente el cliente reciba un 500 en vez del 422 que le corresponde.
Dos herramientas para controlar las trazas. Node guarda 10 marcos por defecto, corto para cadenas asíncronas largas: se amplía con node --stack-trace-limit=50 o con Error.stackTraceLimit = 50. Y Error.captureStackTrace(this, this.constructor) en el constructor de ErrorDeAplicacion elimina de la traza los marcos del propio constructor, un detalle pequeño que hace que tus errores señalen directamente al código que los lanzó.
Errores frecuentes y su diagnóstico
Todos estos han aparecido ya a lo largo del curso. Esta tabla es tu referencia de consulta:
| Error | Causa habitual en Escena Viva | Cómo diagnosticarlo |
|---|---|---|
ERR_HTTP_HEADERS_SENT |
Un res.json() sin return seguido de next(error) |
Punto de interrupción en el segundo envío; busca el return que falta |
EADDRINUSE |
Un npm run dev olvidado, o dos pruebas con puerto fijo |
lsof -i :3000 o ss -ltnp y matar el proceso; puerto efímero en pruebas |
ECONNREFUSED |
Mongo o PostgreSQL parados, o URI equivocada en .env |
Comprueba el servicio y la variable; imprime configuracion al arrancar |
ETIMEDOUT |
Proveedor externo lento, cortafuegos, AbortSignal.timeout corto |
Sube el tiempo para confirmar; después revisa la red, no el timeout |
MODULE_NOT_FOUND |
Extensión olvidada, ruta relativa mal, node_modules desactualizado |
El mensaje trae la ruta buscada: compárala carácter a carácter |
unhandledRejection |
await olvidado, o async sin propagar |
Registra el manejador de process; la traza indica el punto |
CastError |
GET /api/eventos/no-existe con Mongoose |
Validar el parámetro con zod y devolver 400 o 404, no un 500 |
E11000 duplicate key |
Dos usuarios con el mismo correo; semilla ejecutada sin limpiar | El mensaje trae el índice; tradúcelo a ConflictoDeEstado en el repositorio |
| El proceso no termina | Conexión a Mongo sin cerrar, setInterval vivo, servidor escuchando |
why-is-node-running (abajo) |
| La petición se queda colgada | El next() olvidado del módulo 6 |
Punto de registro en cada middleware: el último que imprime es el culpable |
Los dos últimos merecen desarrollo. Con "exit": false en .mocharc.json, el proceso que no termina aparece en cuanto olvidas cerrar algo:
// test/ayudas/preparacion.js, temporalmente mientras investigas
const porQueSigueVivo = require('why-is-node-running');
exports.mochaHooks = {
async afterAll() {
await desconectar();
setTimeout(() => porQueSigueVivo(), 1000).unref();
},
};Imprime cada handle y request abierto con la traza del sitio donde se creó, y suele ser una de tres cosas: la conexión de Mongoose, un setInterval de una caché o de un limpiador de tokens, o un http.Server creado a mano. La alternativa sin instalar nada es imprimir process._getActiveHandles().length en un after: es API interna y sin documentar, pero para depurar sirve.
Para la petición colgada, el diagnóstico más rápido es un middleware temporal de trazado, app.use(trazar('json')), app.use(trazar('autenticar')), etcétera, donde trazar(etiqueta) imprime [${req.idPeticion}] pasa por ${etiqueta} y llama a next(). El último trazar que aparece en la consola señala al middleware siguiente como culpable.
Fugas de memoria
Una fuga es memoria que se retiene y ya no se usa. En un script no importa; en un servidor que corre semanas, acaba en un reinicio por falta de memoria. Los síntomas: la memoria del proceso crece de forma sostenida y no baja tras las horas valle, el rendimiento se degrada poco a poco porque el recolector trabaja cada vez más, y finalmente llega JavaScript heap out of memory.
El primer diagnóstico no necesita herramientas: un setInterval temporal que imprima process.memoryUsage() cada minuto. De sus cuatro campos, rss es la memoria total del proceso, heapTotal lo reservado por V8, external los Buffers, y heapUsed es el que importa. La clave no es el valor absoluto sino la tendencia durante horas: que suba y baje es normal, que solo suba no lo es.
El diagnóstico serio es comparar dos instantáneas del montículo. Arranca con node --inspect, ve a chrome://inspect, pestaña Memory, y toma la instantánea A. Ejercita la aplicación repitiendo varias veces el mismo ciclo —compras, listados, logins— y toma la instantánea B. Selecciona B y cambia la vista a Comparison contra A: la columna Delta muestra qué tipos de objeto han crecido sin liberarse, y si tras diez ciclos idénticos ves 10.000 objetos Sesion más o 500 listeners más, ahí está tu fuga. La sección Retainers te dice quién mantiene viva la referencia, que es la información que realmente necesitas. También puedes generarla desde el código con v8.writeHeapSnapshot(), útil en un servidor sin navegador: devuelve la ruta de un fichero .heapsnapshot que después se descarga y se carga en DevTools.
Las causas típicas, todas ya vistas en este curso:
| Causa | Cómo ocurre en Escena Viva | Solución |
|---|---|---|
| Oyentes no desregistrados (M2) | Cada petición hace gestorDeVentas.on('venta-registrada', ...) y nadie hace off |
Usar once u off explícito; vigilar MaxListenersExceededWarning |
| Caché sin límite (M4) | La caché de cambio-divisas.js guarda cada divisa y nunca expira |
Límite de tamaño (LRU) y caducidad por entrada |
| Variables de módulo que acumulan | Un array de "últimas peticiones" al que solo se hace push |
Tamaño máximo, o mover a Redis (módulo 10) |
| Cierres que retienen | Un callback que captura el req entero y vive en un temporizador |
Capturar solo lo necesario (req.idPeticion, no req) |
| Temporizadores no cancelados | setInterval por sesión de usuario |
clearInterval al terminar; unref() cuando proceda |
El aviso MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 venta-registrada listeners added es un regalo de Node: te está diciendo exactamente dónde mirar. No lo silencies subiendo setMaxListeners; investiga por qué se añaden.
Depurar en producción sin parar el servicio
En producción no puedes poner un punto de interrupción: detendrías las peticiones de todos los usuarios. Las técnicas son otras.
1. Registros estructurados con identificador de petición. Es, con diferencia, la herramienta más valiosa. El middleware id-peticion.js del módulo 6 asigna un identificador único a cada petición y registro-peticiones.js lo incluye en cada línea:
{"nivel":"info","idPeticion":"9f3c1e","metodo":"POST","ruta":"/api/pedidos","usuarioId":"usr-001","mensaje":"compra iniciada"}
{"nivel":"warn","idPeticion":"9f3c1e","codigo":"AFORO_INSUFICIENTE","sesionId":"ses-001-1","libres":2,"solicitadas":4}
{"nivel":"info","idPeticion":"9f3c1e","estado":409,"duracionMs":91}Con ese idPeticion reconstruyes el recorrido completo de una petición concreta entre miles de líneas simultáneas: cuando Lucía escriba diciendo que no pudo comprar a las 18:22, tienes su historia entera filtrando por un campo. La monitorización y la agregación de estos registros se cierran en el módulo 11. Regla de oro: nunca registres datos personales, contraseñas, tokens ni cabeceras Authorization, porque un registro es un fichero que muchas personas pueden leer y que a menudo se envía a un tercero.
2. Activar el inspector con una señal, temporalmente. kill -USR1 <pid> hace que Node abra el inspector en el 9229 sin reiniciar el proceso, y te conectas por un túnel SSH: ssh -L 9229:localhost:9229 usuario@servidor, y después chrome://inspect en tu máquina local ve el proceso remoto.
3. Nunca dejes --inspect abierto en un servidor público. Esto es una advertencia de seguridad seria, no una recomendación de estilo: el puerto del inspector no tiene autenticación, así que cualquiera que llegue a él puede evaluar código arbitrario en tu proceso, leer memoria —incluidos los secretos JWT y las contraseñas de la base de datos— y modificar el comportamiento en vivo. Es ejecución remota de código servida en bandeja. Las reglas mínimas: nunca --inspect=0.0.0.0:9229 (por defecto escucha solo en 127.0.0.1, déjalo así), accede siempre por túnel SSH, ciérralo en cuanto termines, y que el cortafuegos bloquee el 9229 desde el exterior como red de seguridad.
4. Volcados de memoria bajo demanda, mediante un endpoint interno protegido por rol de administrador y accesible solo desde la red privada que llame a v8.writeHeapSnapshot(). Permite investigar una fuga en producción sin tocar el servicio.
Conviene dejar clara la frontera, porque la confusión es habitual:
| Depuración (este módulo) | Perfilado (módulo 10) | |
|---|---|---|
| Pregunta | ¿Por qué hace algo incorrecto? | ¿Por qué tarda tanto? |
| Síntoma | Error, resultado erróneo, cuelgue | Lentitud, poca capacidad |
| Herramientas | Inspector, puntos de interrupción, trazas, registros | Perfiles de CPU, flamegraphs, autocannon, clinic |
| Resultado | Un fallo localizado y una prueba que lo reproduce | Un cuello de botella cuantificado |
Método de depuración
Las herramientas sin método producen tardes perdidas. Este es el procedimiento, en orden.
1. Reproducir. Un fallo que no puedes reproducir no puedes arreglarlo; a lo sumo puedes cambiar cosas hasta que parezca que desaparece. Reúne petición exacta, usuario y rol, datos implicados, hora e idPeticion; si es intermitente, ejecútalo en bucle hasta que caiga.
2. Aislar. Reduce hasta el mínimo que sigue fallando: ¿falla sin autenticación?, ¿con otra sesión?, ¿solo con evt-002? Cada respuesta elimina medio bosque.
3. Formular una hipótesis falsable. No "algo raro pasa con el aforo", sino "creo que vender() compara con < en lugar de <= y por eso rechaza la última entrada". Una hipótesis que no se puede refutar no sirve de nada.
4. Comprobarla con un punto de interrupción, un punto de registro o una consulta. Si era falsa, no la retuerzas: formula otra. Aferrarse a una hipótesis equivocada es la causa número uno de las depuraciones largas.
5. Bisección con git bisect cuando sepas que antes funcionaba:
git bisect start
git bisect bad # el commit actual falla
git bisect good v1.4.0 # esta versión funcionaba
git bisect run npm run test:unidad # automático: Git usa el código de salida
git bisect resetCon 1000 commits entre el bueno y el malo, git bisect encuentra el culpable en 10 pasos, y con run se va a por un café y vuelve con el commit exacto. Fíjate en lo que implica: git bisect automático solo funciona si tienes pruebas. Es otro beneficio de este módulo que no se ve hasta que lo necesitas.
6. Escribir una prueba que reproduzca el fallo, antes de arreglarlo. Este paso cierra el círculo del módulo entero y es innegociable. El orden importa: escribes la prueba y debe fallar —si pasa, no has entendido el fallo y estás a punto de arreglar otra cosa—, después arreglas el código, la prueba pasa, y por último ejecutas la batería completa para comprobar que el arreglo no rompió nada más.
// Incidencia 481: un pedido de exactamente las últimas localidades devolvía 409.
// Esta prueba falló en rojo antes del arreglo (vender comparaba con < en vez de <=).
it('permite comprar exactamente las ultimas localidades libres', async () => {
const lucia = await comoAsistente();
await ajustarAforo('ses-001-1', { libres: 2 });
const { body } = await request(app).post('/api/pedidos').set(...lucia.cabecera)
.send({ sesionId: 'ses-001-1', cantidad: 2 }).expect(201);
expect(body.entradas).to.have.lengthOf(2);
const sesion = await request(app).get('/api/sesiones/ses-001-1').expect(200);
expect(sesion.body.libres).to.equal(0);
expect(sesion.body.agotada).to.be.true;
});Tres cosas ganas con esa prueba: demuestras que entendiste el fallo, garantizas que no volverá nunca y dejas documentado un caso límite que a nadie se le había ocurrido. Un fallo en producción es caro; desperdiciarlo sin convertirlo en una prueba es tirar el dinero.
Errores Comunes y Consejos
- Depurar sin reproducir. Cambiar código hasta que "parece que ya va" no arregla nada: lo esconde hasta el peor momento posible.
- Olvidar
--timeout 0al depurar pruebas. Mocha aborta la prueba mientras estás parado leyendo variables y crees que el depurador está roto. - No usar
skipFiles. Pulsas F11 y acabas dentro deexpress/lib/router/layer.jssin saber cómo salir. catchvacíos. Borran la única información que tenías; registra siempre, aunque sea en niveldebug.- Perder la causa al reenvolver errores. Usa
{ cause: error }: la traza original vale más que un mensaje bonito. - Silenciar
MaxListenersExceededWarning. Es un detector de fugas gratuito; subir el límite es tapar el aviso de incendio. - Dejar
--inspecten un servidor accesible. Es ejecución remota de código sin autenticación. Túnel SSH siempre. - Registrar tokens o contraseñas. Un registro es un fichero que mucha gente lee y que suele salir de tu infraestructura.
- Consejo: cuando una prueba de integración falle con 500, imprime
respuesta.bodyy busca elidPeticionen la salida del servidor: tienes la historia completa en dos pasos. Y aprende bien los puntos de interrupción condicionales, que es la habilidad que más tiempo ahorra de toda la lección.
Ejercicios
Ejercicio 1: diagnosticar sin ejecutar
Para cada síntoma, indica la causa más probable, la herramienta con la que lo confirmarías y el arreglo:
npm testimprime74 passingy el proceso se queda colgado sin devolver el prompt.POST /api/pedidosresponde 500 conCannot set headers after they are sent to the client.GET /api/eventos/holadevuelve 500 en lugar de 404.- El servidor lleva cinco días arriba y
heapUsedha pasado de 90 MB a 780 MB. - Una prueba de integración pasa sola y falla dentro de la batería completa.
Ejercicio 2: depurar una prueba con VS Code
Toma la prueba de concurrencia de 09-04. Configura .vscode/launch.json con la entrada "Mocha: el fichero abierto", coloca un punto de interrupción condicional dentro del repositorio de pedidos que solo se active cuando libres < 2, y describe qué verías en el panel de pila de llamadas y en el de variables. Explica por qué --timeout 0 es imprescindible aquí.
Ejercicio 3: del fallo a la prueba
Un organizador de la Sala Bóveda informa de que el informe de evt-002 muestra ingresosCentimos: NaN desde ayer. Diseña el proceso completo: cómo reproducirlo, cómo aislarlo, dos hipótesis falsables, cómo usarías git bisect, y escribe la prueba que reproduce el fallo (debe fallar antes del arreglo).
Soluciones
Ejercicio 1. (1) Un handle abierto, casi seguro la conexión a Mongo sin cerrar; confírmalo con why-is-node-running en afterAll y arréglalo con await desconectar() en el gancho raíz. (2) Doble respuesta: un manejador hace res.json(...) y luego llama a next(error), o hay un await tras responder que lanza; punto de interrupción en manejadorDeErrores y mira la pila; el arreglo es return res.json(...). (3) CastError de Mongoose: hola no es un ObjectId válido y el error llega como desconocido, que ESTADO_POR_CODIGO traduce a 500; valida el parámetro con zod o traduce CastError a RecursoNoEncontrado en el repositorio. (4) Fuga de memoria: dos instantáneas del montículo comparadas tras ciclos idénticos, con los oyentes acumulados en GestorDeVentas y la caché sin límite de cambio-divisas.js como principales sospechosos. (5) Contaminación entre pruebas: un stub sin restaurar, un reloj falso vivo o datos que otra prueba dejó; confírmalo ejecutando la batería en orden inverso y arréglalo con sinon.restore() en el gancho raíz y limpieza en afterEach.
Ejercicio 2. El punto de interrupción condicional se coloca en la línea del repositorio donde se comprueba el aforo, con la condición sesion.libres < 2. En el panel de pila de llamadas verías, de arriba abajo, el método del repositorio, comprarEntradas, el controlador crearPedido y varios marcos de Express separados por marcadores --- await ---; haciendo clic en el marco del controlador verías req.usuario y req.datosValidados de esa petición concreta. En el panel de variables verías sesion.libres con el valor exacto en el instante en que una de las diez compras simultáneas encuentra el aforo casi agotado, y podrías comprobar en la consola si la comparación con cantidad da lo que esperabas.
--timeout 0 es imprescindible por una razón específica: la prueba lanza diez peticiones con Promise.all, y mientras estás parado en una las otras nueve siguen esperando; con el timeout normal de 5000 ms, Mocha abortaría la prueba entera a los cinco segundos, cerrando la aplicación bajo tus pies.
Ejercicio 3. Reproducir: llamar a GET /api/eventos/evt-002/informe con un organizador de org-boveda en la base de pruebas sembrada; si no se reproduce, copiar los datos reales de evt-002, porque entonces el fallo está en los datos y no en el código. Aislar: probar con evt-001 y evt-003; si solo falla evt-002 —el de las tres sesiones—, calcular el informe sesión a sesión para encontrar cuál produce NaN. Hipótesis falsables: que una sesión tenga precioCentimos nulo y el reduce propague NaN (porque undefined * 2 es NaN y NaN contamina toda la suma), o que el informe sume precioEuros en lugar de precioCentimos. Bisección: como "desde ayer" acota la ventana, git bisect start, bad HEAD, good <commit de anteayer> y git bisect run npm run test:unidad.
it('calcula los ingresos aunque una sesion no tenga precio asignado', () => {
const evento = crearEvento({
id: 'evt-002',
sesiones: [
{ vendidas: 120, precioCentimos: 1800 },
{ vendidas: 95, precioCentimos: 1800 },
{ vendidas: 60, precioCentimos: undefined }, // la sesión problemática
],
});
expect(Number.isNaN(evento.ingresosCentimos)).to.be.false;
expect(evento.ingresosCentimos).to.equal(387000); // (120 + 95) * 1800
});Esta prueba falla en rojo antes del arreglo, que tiene dos partes y ambas importan: tratar el precio ausente como cero en el cálculo (s.precioCentimos ?? 0) y hacer que el modelo exija precioCentimos, para que el dato inválido no pueda volver a entrar. Arreglar solo el síntoma dejaría la puerta abierta.
Conclusión
Con esta lección se cierra el módulo 9, y con él se cierra una brecha que llevaba abierta desde el módulo 5.
Escena Viva ya no descansa sobre la confianza. npm test está verde y significa algo: el dominio calcula bien el aforo y los totales en céntimos, la matriz de permisos está recorrida celda por celda —aquel if invertido en puedeGestionarEvento que abriría el catálogo entero ahora pone varias pruebas en rojo—, el conversor de divisas degrada con elegancia cuando el proveedor se cae, la API responde 401, 403, 404, 409, 422 y 400 con el formato de error acordado, un asistente no puede ver el pedido de otro, y diez compras simultáneas por cinco butacas venden exactamente cinco butacas. La cobertura está medida con umbrales que solo suben, los scripts están organizados, y el proyecto puede ejecutarse solo en un servidor de integración continua. Y cuando algo falle —porque algo fallará—, sabes reproducirlo, aislarlo, ponerle un punto de interrupción condicional, leer su traza entre el ruido de node_modules, encontrar el commit culpable con git bisect y convertirlo en una prueba que impida su regreso.
Esa es la red de seguridad. A partir de ahora puedes cambiar el código sin miedo, que era exactamente la promesa con la que abrimos la primera lección.
Y sin embargo, la aplicación tiene un problema del que las pruebas no dicen ni una palabra, porque no es un problema de corrección: es correcta, pero es lenta y desaprovecha la máquina. Escena Viva usa un solo núcleo mientras el servidor tiene ocho, y en el estreno del Festival de Jazz de Primavera todos los procesos se pelearán por ese único hilo. Recalcula el mismo catálogo cientos de veces por minuto para devolver siempre lo mismo. Y cuando genera el PDF de una entrada, bloquea el bucle de eventos el tiempo suficiente para que las demás peticiones se queden esperando en la cola.
En el módulo 10, Temas Avanzados, atacamos justo eso: cluster para usar todos los núcleos, worker threads para sacar la generación de PDF del hilo principal, Redis para cachear el catálogo y encolar el trabajo pesado, optimización del rendimiento con perfiles de CPU, flamegraphs y pruebas de carga —la frontera que hemos respetado en este módulo—, y por último el diseño de APIs RESTful maduras y una introducción a GraphQL.
Curso de Node.js: De Principiante a Avanzado
Módulo 1: Introducción a Node.js
- ¿Qué es Node.js?
- Instalación y Configuración del Entorno
- Tu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Moderno para Node.js
- El Proyecto del Curso: la Plataforma Escena Viva
Módulo 2: Conceptos Básicos
- Arquitectura de Node.js
- El Bucle de Eventos (Event Loop)
- Callbacks y Programación Asíncrona
- Promesas y async/await
- Eventos y EventEmitter
- Módulos CommonJS y require()
- Módulos ES e Interoperabilidad
Módulo 3: Sistema de Archivos y E/S
- Lectura y Escritura de Archivos
- El Módulo fs a Fondo
- Rutas Multiplataforma con el Módulo path
- Trabajando con Streams
- Streams de Transformación y pipeline
- Buffers y Datos Binarios
Módulo 4: HTTP y Servidores Web
- Creando un Servidor HTTP Simple
- Manejo de Solicitudes y Respuestas
- Enrutamiento Manual
- Sirviendo Archivos Estáticos
- Recibiendo Datos: Cuerpos de Petición y JSON
- Consumiendo APIs Externas desde Node.js
Módulo 5: NPM y Gestión de Paquetes
- Introducción a NPM y package.json
- Instalación y Uso de Paquetes
- Versionado Semántico y package-lock
- Scripts de npm y Automatización del Proyecto
- Creación y Publicación de Paquetes
- Seguridad y Mantenimiento de Dependencias
Módulo 6: Framework Express.js
- Introducción a Express.js
- Configuración de una Aplicación Express
- Enrutamiento en Express
- Middleware
- Middleware de Terceros Esenciales
- Validación de Datos de Entrada
- Manejo de Errores
Módulo 7: Bases de Datos y ORMs
- Introducción a las Bases de Datos
- Usando MongoDB con Mongoose
- Operaciones CRUD
- Relaciones, Poblado y Consultas Avanzadas
- Usando Bases de Datos SQL con Sequelize
- Migraciones, Transacciones y Datos de Prueba
Módulo 8: Autenticación y Autorización
- Introducción a la Autenticación
- Registro de Usuarios y Hash de Contraseñas
- Sesiones y Cookies con Passport.js
- Autenticación con JWT
- Control de Acceso Basado en Roles
- Buenas Prácticas de Seguridad en APIs
Módulo 9: Pruebas y Depuración
- Introducción a las Pruebas
- Pruebas Unitarias con Mocha y Chai
- Dobles de Prueba con Sinon
- Pruebas de Integración
- Cobertura y Automatización de las Pruebas
- Depuración de Aplicaciones Node.js
Módulo 10: Temas Avanzados
- El Módulo Cluster
- Hilos de Trabajo (Worker Threads)
- Caché y Colas de Trabajo con Redis
- Optimización del Rendimiento
- Construcción de APIs RESTful
- GraphQL con Node.js
Módulo 11: Despliegue y DevOps
- Configuración y Variables de Entorno
- Registro y Monitorización en Producción
- Usando PM2 para la Gestión de Procesos
- Empaquetado con Docker
- Desplegando en Heroku y Otras PaaS
- Integración y Despliegue Continuos
