En la lección anterior probamos el dominio puro de Escena Viva en cincuenta y dos milisegundos. Fue fácil porque Sesion, Evento y politica.js no dependen de nada: les das datos, te devuelven resultados. Toda la aplicación real, en cambio, está llena de dependencias que rompen el determinismo.
src/servicios/cambio-divisas.jsllama a una API externa confetch: si el proveedor está caído, la prueba falla sin que nuestro código tenga ningún fallo, y si funciona tarda 400 ms y consume cuota.generarCodigoEntradaconstruye códigosEV-<año>-<6 dígitos>con el año actual: esa prueba pasará hasta el 31 de diciembre y fallará el 1 de enero.- Los tokens de acceso caducan a los 15 minutos. Probar la caducidad tal cual requeriría una prueba de quince minutos.
- Los repositorios abren conexiones a Mongo. Una prueba unitaria de un controlador no debería necesitar una base de datos.
La solución son los dobles de prueba: objetos que sustituyen a las dependencias reales durante la prueba, igual que un doble de acción sustituye al actor en la escena peligrosa. En Node la herramienta estándar es Sinon.
Contenido
- Taxonomía de los dobles de prueba
sinon.spy: observar sin cambiarsinon.stub: sustituir el comportamiento- Restaurar siempre: sandboxes y contaminación
- Relojes falsos
- Simular
fetchpara probarcambio-divisas.js - Simular el repositorio para probar un controlador
- Inyección de dependencias frente a parcheo de módulos
- Cuándo NO usar dobles
Taxonomía de los dobles de prueba
La terminología viene de Gerard Meszaros y aparece en toda la literatura y en los nombres de la API.
| Doble | Qué es | Cuándo usarlo | En Sinon |
|---|---|---|---|
| Dummy | Un valor que se pasa solo para rellenar un parámetro; nunca se usa | Un argumento obligatorio irrelevante para la prueba | {}, null, () => {} |
| Stub | Sustituye la dependencia devolviendo respuestas preprogramadas | Controlar qué entra en la unidad bajo prueba | sinon.stub() |
| Spy | Envuelve la función real y registra cómo se llamó, sin cambiarla | Verificar qué salió de la unidad, manteniendo el comportamiento | sinon.spy() |
| Mock | Stub con expectativas declaradas por adelantado que se verifican al final | Cuando la interacción es el comportamiento a probar | sinon.mock() |
| Fake | Implementación simplificada pero funcional | Un repositorio en memoria, una cola de mentira | Clase propia, sinon.fake() |
La distinción práctica que más usarás es stub frente a spy: un spy te deja preguntar ¿se llamó a esto, con qué argumentos y cuántas veces? sin alterar nada; un stub además decide qué devuelve, y por tanto controla el flujo de la unidad bajo prueba. Regla de oro: stubs para las entradas (lo que la unidad recibe del mundo) y spies para las salidas (lo que la unidad hace hacia el mundo).
Evita los mock clásicos de Sinon: sus expectativas por adelantado producen pruebas frágiles y difíciles de leer. En la práctica, stub más aserciones al final cubre todo lo que necesitas.
sinon.spy: observar sin cambiar
Un espía envuelve una función y la deja hacer su trabajo, registrando cada llamada.
// Función suelta: el doble ES el callback
const alRegistrarVenta = sinon.spy();
gestor.on('venta-registrada', alRegistrarVenta);
gestor.registrarVenta(sesion, 2);
expect(alRegistrarVenta.calledOnce).to.be.true;
// Método de un objeto existente: mantiene el comportamiento real
const espiaVender = sinon.spy(sesion, 'vender');
sesion.vender(3);
expect(espiaVender.calledWith(3)).to.be.true;
expect(sesion.libres).to.equal(297); // el método real SÍ se ejecutó| Propiedad / método | Qué comprueba |
|---|---|
called, calledOnce, callCount |
Se llamó; una sola vez; número exacto |
calledWith(a, b) / calledWithExactly(a, b) |
Alguna llamada recibió esos argumentos (comparación estructural), o exactamente esos y ninguno más |
firstCall.args, lastCall.args, getCall(n).args |
Argumentos de una llamada concreta |
calledBefore(otro), threw(), returnValues |
Orden relativo entre espías; si lanzó; qué devolvió |
Un consejo sobre los mensajes de fallo: expect(espia.calledWith('compra')).to.be.true falla con expected false to be true, que no dice nada. Prefiere comparar los argumentos (expect(espia.firstCall.args[0]).to.equal('compra'), que falla con expected 'refresco' to equal 'compra') o usar las aserciones propias de Sinon, que imprimen las llamadas registradas:
sinon.match permite aserciones parciales muy útiles cuando el argumento tiene campos volátiles: sinon.match.string, sinon.match.number, sinon.match.instanceOf(Evento), sinon.match.has('codigo', 'AFORO_INSUFICIENTE').
sinon.stub: sustituir el comportamiento
Un stub reemplaza la implementación por completo. Su repertorio:
const repo = { buscarPorId: () => {}, guardar: () => {} };
sinon.stub(repo, 'buscarPorId').returns(crearEvento()); // valor síncrono
sinon.stub(repo, 'buscarPorId').resolves(crearEvento()); // promesa resuelta
sinon.stub(repo, 'guardar').rejects(new ConflictoDeEstado('Aforo insuficiente'));
sinon.stub(repo, 'guardar').throws(new ErrorDeValidacion('Cantidad invalida'));
// Comportamiento distinto según la llamada: oro puro para probar reintentos
const buscar = sinon.stub();
buscar.onCall(0).rejects(new Error('ETIMEDOUT'));
buscar.onCall(1).rejects(new Error('ETIMEDOUT'));
buscar.onCall(2).resolves({ tasa: 1.09 });
// Comportamiento según los argumentos, con caso por defecto
const eventos = { buscarPorId: sinon.stub() };
eventos.buscarPorId.withArgs('evt-001').resolves(crearEvento());
eventos.buscarPorId.resolves(null);
sinon.stub(servicio, 'calcular').callThrough(); // deja pasar al original
sinon.stub(repo, 'guardar').callsFake(async (p) => ({ ...p, id: 'ped-999' }));Un stub también es un spy: conserva calledOnce, firstCall.args y todo el repertorio anterior. Por eso en la práctica casi siempre usarás stub.
sinon.fake es una API más moderna y minimalista con la misma idea (sinon.fake.returns(...), sinon.fake.resolves(...), sinon.fake.throws(...)). Es inmutable —no se reprograma después de crearse— y eso la hace más predecible; funciona muy bien como callback anónimo, mientras que para sustituir métodos de objetos existentes stub sigue siendo lo más cómodo.
Restaurar siempre: sandboxes y contaminación
Aquí está el fallo más difícil de diagnosticar de todo el módulo. sinon.stub(objeto, 'metodo') modifica el objeto real en memoria, y en Node require cachea los módulos: el auditoria.js que ve tu prueba es exactamente el mismo objeto que ven todas las demás pruebas del proceso. Si no lo restauras, el stub sigue ahí en el fichero siguiente.
it('registra la compra', () => { // test/unidad/compras.test.js
sinon.stub(auditoria, 'registrar').resolves(); // sin restaurar
});
// En test/unidad/tokens.test.js, más tarde y en el mismo proceso,
// auditoria.registrar SIGUE SIENDO EL STUB de la otra prueba.Los síntomas son inconfundibles y desesperantes: la prueba pasa sola pero falla en la batería completa, falla solo cuando se ejecuta después de otra concreta, o aparece TypeError: Attempted to wrap registrar which is already wrapped.
La defensa es sinon.restore() en afterEach, que deshace todo lo creado con el sandbox por defecto (sinon.stub, sinon.spy, sinon.useFakeTimers). Cuando quieras un ámbito propio, sinon.createSandbox() te da una caja con su propio caja.restore(). Y para que nadie pueda olvidarlo, se pone en un gancho raíz cargado desde .mocharc.json:
// test/ayudas/preparacion.js
'use strict';
const sinon = require('sinon');
// Gancho raíz de Mocha: se aplica a TODAS las pruebas del proyecto
exports.mochaHooks = {
afterEach() {
sinon.restore();
},
};{ "spec": ["test/**/*.test.js"], "recursive": true, "timeout": 5000,
"forbidOnly": true, "require": ["test/ayudas/preparacion.js"] }Relojes falsos
sinon.useFakeTimers() sustituye Date, setTimeout, setInterval, setImmediate y process.hrtime por versiones controladas. El tiempo deja de correr solo: avanza cuando tú se lo dices con clock.tick().
Caso 1: la caducidad de un token de acceso de 15 minutos
describe('tokens de acceso', () => {
let reloj;
beforeEach(() => {
reloj = sinon.useFakeTimers(new Date('2026-10-01T10:00:00.000Z'));
});
afterEach(() => {
reloj.restore(); // IMPRESCINDIBLE: si no, el resto de pruebas vive en 2026-10-01
});
it('sigue aceptando el token a los 14 minutos', () => {
const token = firmarAcceso({ id: 'usr-001', rol: 'asistente' });
reloj.tick(14 * 60 * 1000);
expect(verificarAcceso(token).id).to.equal('usr-001');
});
it('rechaza el token pasados 15 minutos y un segundo', () => {
const token = firmarAcceso({ id: 'usr-001', rol: 'asistente' });
reloj.tick(15 * 60 * 1000 + 1000);
let capturado = null;
try {
verificarAcceso(token);
expect.fail('Se esperaba ErrorDeAutenticacion por token caducado');
} catch (error) { capturado = error; }
expect(capturado).to.be.instanceOf(ErrorDeAutenticacion);
expect(capturado.codigo).to.equal('TOKEN_CADUCADO');
});
});Dos pruebas que cubren el antes y el después de la caducidad, ejecutadas en menos de un milisegundo. Sin el reloj falso, la segunda sería literalmente imposible de escribir. Funciona porque jsonwebtoken usa Date.now() internamente y el reloj falso también afecta a la firma; si una librería usara una fuente de tiempo que Sinon no intercepta, tendrías que inyectarle el reloj.
Caso 2: los reintentos con espera del módulo 2
Probar reintentar(operacion, { intentos: 3, esperaMs: 1000 }) de verdad costaría dos segundos por prueba. Con el reloj falso, cero:
it('reintenta hasta tres veces con espera exponencial', async () => {
const reloj = sinon.useFakeTimers();
const operacion = sinon.stub();
operacion.onCall(0).rejects(new Error('ETIMEDOUT'));
operacion.onCall(1).rejects(new Error('ETIMEDOUT'));
operacion.onCall(2).resolves({ tasa: 1.09 });
const promesa = reintentar(operacion, { intentos: 3, esperaMs: 1000 });
await reloj.tickAsync(1000); // avanza el tiempo Y deja correr las microtareas
await reloj.tickAsync(2000);
expect(await promesa).to.deep.equal({ tasa: 1.09 });
expect(operacion.callCount).to.equal(3);
reloj.restore();
});La clave es tickAsync en lugar de tick: tick avanza los temporizadores de forma síncrona, pero entre dos reintentos hay promesas pendientes que necesitan que el bucle de eventos ceda el turno, y tickAsync lo permite. Advertencia repetida a propósito: si no restauras el reloj, todas las pruebas posteriores del proceso viven en un tiempo congelado, los timeouts de Mocha se comportan de forma extrañísima y pasarás una tarde entera sin entender nada. El gancho raíz con sinon.restore() te cubre también aquí.
Simular fetch para probar cambio-divisas.js
Este servicio es el ejemplo perfecto de dependencia externa: llama por HTTP a un proveedor de tasas, con AbortSignal.timeout, reintentos y caché en memoria. Queremos probar sus comportamientos sin tocar la red.
/** Construye una respuesta parecida a la de fetch. */
function respuestaFalsa({ estado = 200, cuerpo = {} } = {}) {
return { ok: estado >= 200 && estado < 300, status: estado, json: async () => cuerpo };
}
describe('cambio de divisas', () => {
beforeEach(() => {
vaciarCache(); // la caché es estado compartido entre pruebas: hay que limpiarla
});
it('devuelve la tasa cuando el proveedor responde correctamente', async () => {
sinon.stub(global, 'fetch').resolves(
respuestaFalsa({ cuerpo: { base: 'EUR', tasas: { GBP: 0.86 } } }));
expect(await obtenerTasa('GBP')).to.equal(0.86);
});
it('degrada elegantemente devolviendo null si el proveedor responde 500', async () => {
sinon.stub(global, 'fetch').resolves(respuestaFalsa({ estado: 500 }));
// La compra no debe romperse porque el conversor esté caído:
// el precio se muestra en euros y punto.
expect(await obtenerTasa('GBP')).to.be.null;
});
it('degrada elegantemente si se agota el tiempo de espera', async () => {
const abortado = new Error('The operation was aborted');
abortado.name = 'AbortError';
sinon.stub(global, 'fetch').rejects(abortado);
expect(await obtenerTasa('GBP')).to.be.null;
});
});A estas se añade la de la caché (dos consultas seguidas y expect(peticion.calledOnce).to.be.true). Cuatro escenarios —dos de ellos, el 500 y el tiempo agotado, imposibles de reproducir a voluntad contra el proveedor real— probados en milisegundos, sin red, sin cuota y sin intermitencia.
Un detalle de higiene fácil de pasar por alto: el vaciarCache() del beforeEach. La caché del servicio es estado compartido dentro del mismo proceso; sin vaciarla, la segunda prueba respondería desde la caché de la primera, fetch no se llamaría y el fallo sería desconcertante. Todo módulo con estado en memoria necesita una función de reinicio pensada para las pruebas, porque Sinon no puede saber de ella.
Simular el repositorio para probar un controlador
Un controlador de Express es una función con tres argumentos: podemos probarlo sin base de datos y sin HTTP dándole dobles.
/** Dobles mínimos de req, res y next. */
function crearContexto({ params = {}, usuario = null } = {}) {
const res = { codigo: null, cuerpo: null,
status(c) { this.codigo = c; return this; },
json(c) { this.cuerpo = c; return this; } };
return { req: { params, usuario }, res, next: sinon.spy() };
}
it('responde 200 con el evento serializado', async () => {
const repositorio = { buscarPorId: sinon.stub().resolves(crearEvento({ id: 'evt-001' })) };
const { req, res, next } = crearContexto({ params: { id: 'evt-001' } });
await obtenerEvento({ repositorioEventos: repositorio })(req, res, next);
expect(repositorio.buscarPorId.calledWith('evt-001')).to.be.true;
expect(res.codigo).to.equal(200);
expect(next.called).to.be.false;
});
it('delega en next con RecursoNoEncontrado si el evento no existe', async () => {
const repositorio = { buscarPorId: sinon.stub().resolves(null) };
const { req, res, next } = crearContexto({ params: { id: 'evt-999' } });
await obtenerEvento({ repositorioEventos: repositorio })(req, res, next);
expect(next.calledOnce).to.be.true;
expect(next.firstCall.args[0]).to.be.instanceOf(RecursoNoEncontrado);
expect(next.firstCall.args[0].codigo).to.equal('EVENTO_NO_ENCONTRADO');
});La segunda prueba comprueba que el controlador delega el error en next en lugar de responder él mismo. Ese es el contrato que hace funcionar manejadorDeErrores del módulo 6, y es la clase de detalle que se rompe cuando alguien "simplifica" el controlador.
Inyección de dependencias frente a parcheo de módulos
Habrás notado la firma obtenerEvento({ repositorioEventos })(req, res, next). No es casualidad: es una fábrica de controladores que recibe sus dependencias. Compara las dos formas de escribir lo mismo.
// DIFÍCIL DE PROBAR: el controlador decide de dónde saca los datos
const { repositorioEventos } = require('../repositorios/index.js');
async function obtenerEvento(req, res, next) {
const evento = await repositorioEventos.buscarPorId(req.params.id);
}
// FÁCIL DE PROBAR: fábrica que recibe sus dependencias
function obtenerEvento({ repositorioEventos }) {
return async function manejador(req, res, next) {
try {
const evento = await repositorioEventos.buscarPorId(req.params.id);
if (!evento) throw new RecursoNoEncontrado('Evento no encontrado', {
codigo: 'EVENTO_NO_ENCONTRADO', detalles: { id: req.params.id } });
res.status(200).json(evento.aJSON());
} catch (error) { next(error); }
};
}En el enrutador solo cambia una línea: enrutador.get('/:id', obtenerEvento({ repositorioEventos })). Esta es una refactorización pequeña y honesta: el código de producción se comporta igual, la aplicación sigue cableando el repositorio real una sola vez, y a cambio el controlador se vuelve trivialmente probable. Ese es el sentido de "las pruebas ejercen presión de diseño": el código difícil de probar suele estar demasiado acoplado, y arreglarlo mejora el código, no solo la prueba.
Con la versión rígida, además, sinon.stub sobre el objeto exportado puede no tener efecto, porque la desestructuración del require ya capturó la referencia. Cuando no puedas refactorizar —código de terceros, módulo legado— queda el último recurso, proxyquire, que intercepta los require de un módulo:
const { obtenerEvento } = proxyquire('../../src/controladores/eventos.js', {
'../repositorios/index.js': { repositorioEventos: { buscarPorId: sinon.stub().resolves(null) } },
});Sus inconvenientes justifican que sea el último recurso: acopla la prueba a las rutas de require (mueves un fichero y se rompe sin que el comportamiento cambie), salta el caché de módulos con efectos raros si hay estado, oculta el problema de diseño en vez de resolverlo, y no funciona con ESM, así que es un callejón sin salida si algún día migras.
Cuándo NO usar dobles
Los dobles son tan cómodos que es fácil pasarse, y una prueba con demasiados solo comprueba tus propias suposiciones. Mira esta joya:
// PRUEBA INÚTIL: no detecta ningún fallo real
it('crea el pedido', async () => {
const repositorio = { crearPedido: sinon.stub().resolves({ totalCentimos: 5000 }) };
const servicio = crearServicioDePedidos({ repositorio });
const pedido = await servicio.comprarEntradas({ sesionId: 'ses-001-1', cantidad: 2 });
expect(pedido.totalCentimos).to.equal(5000);
});¿Qué prueba esto? Que un stub que devuelve 5000 devuelve 5000. Si comprarEntradas calculara mal el total, pasaría. Si no comprobara el aforo, pasaría. Si vendiera entradas negativas, pasaría. Cuesta mantenerla y no puede ponerse roja por un fallo real.
Y hay un problema peor: el doble puede mentir sobre la dependencia real. Si tu stub del repositorio devuelve null cuando no encuentra el evento pero el repositorio real lanza CastError con un id malformado, tus pruebas unitarias estarán todas verdes mientras la API devuelve 500 en producción. El doble codifica lo que tú crees que hace la dependencia.
| Situación | ¿Doblar? |
|---|---|
| Red, servicios externos, pasarelas de pago | Sí, siempre en pruebas unitarias |
| Reloj, aleatoriedad, identificadores generados | Sí |
| Escenarios imposibles de provocar (500, timeout, disco lleno) | Sí, es la única forma |
| Operaciones deliberadamente lentas (bcrypt coste 12 en bucle) | Sí, con criterio |
Objetos de valor y del dominio (Sesion, Evento) |
No, son baratos y reales |
| Lógica pura de tu propio código | No, nunca |
| La base de datos en una prueba de integración | No, ahí es justo lo que quieres probar |
La regla que lo resume: dobla el borde del sistema, no su interior. Y acepta que las pruebas unitarias con dobles, por muchas que sean, no demuestran que las piezas encajen. Las pruebas de integración de la lección siguiente son el contrapeso necesario; sin ellas, los dobles se convierten en un espejo donde tu código se admira a sí mismo.
Errores Comunes y Consejos
- Olvidar
sinon.restore(). Contaminación entre ficheros,already wrapped, pruebas que fallan solo en batería. Pon el gancho raíz entest/ayudas/preparacion.jsy olvídate. - No restaurar el reloj falso. Todo el proceso queda congelado en el tiempo: el fallo más desconcertante que existe.
- Usar
tickdonde hace faltatickAsync. Los reintentos con promesas no avanzan y la prueba muere por timeout. - Stub sobre una referencia desestructurada.
const { registrar } = require('./auditoria.js')captura la función;sinon.stub(auditoria, 'registrar')cambia la propiedad del objeto, no tu copia. Llama siempre a través del objeto. expect(espia.calledWith(x)).to.be.true. Mensaje de fallo inútil: usasinon.assert.calledWitho comparafirstCall.args.- Olvidar limpiar el estado en memoria (la caché de
cambio-divisas.js). Sinon no lo sabe; hay que hacerlo a mano. - Simular tu propio dominio. Si estás doblando
Sesion, párate: es barata, determinista y real. - Consejo: cuando un stub necesite más de tres
withArgs, plantéate un fake de verdad; una clase pequeña con unMapdentro suele ser más legible. - Consejo: cada vez que escribas un stub, pregúntate "¿qué fallo real detectaría esta prueba?". Si no sabes responder, sobra.
Ejercicios
Ejercicio 1: el año del código de entrada
generarCodigoEntrada() produce códigos EV-<año>-<6 dígitos> con el año actual y seis dígitos aleatorios. Escribe pruebas deterministas que comprueben: que el año del código es el año en curso congelando el reloj en 2026-11-05, que el formato cumple exactamente el patrón, y que dos códigos generados seguidos son distintos.
Ejercicio 2: la degradación del conversor en el controlador
El controlador de eventos muestra el precio también en libras cuando el usuario pide ?divisa=GBP. Escribe dos pruebas unitarias con dobles: una en la que obtenerTasa resuelve 0.86 y la respuesta incluye precioGbp, y otra en la que resuelve null y la respuesta no incluye precioGbp pero sigue siendo un 200 correcto.
Ejercicio 3: detectar la prueba inútil
Explica por qué esta prueba no aporta nada y reescríbela para que detecte un fallo real:
it('aplica el descuento de socio', () => {
const calculadora = { aplicarDescuento: sinon.stub().returns(4500) };
expect(calculadora.aplicarDescuento(5000, 'socio')).to.equal(4500);
});Soluciones
Ejercicio 1
beforeEach(() => sinon.useFakeTimers(new Date('2026-11-05T12:00:00.000Z')));
it('usa el ano en curso en el prefijo del codigo', () => {
expect(generarCodigoEntrada()).to.match(/^EV-2026-/);
});
it('cumple el formato EV-<ano>-<6 digitos>', () => {
expect(generarCodigoEntrada()).to.match(/^EV-\d{4}-\d{6}$/);
});
it('genera codigos distintos en llamadas consecutivas', () => {
expect(new Set([generarCodigoEntrada(), generarCodigoEntrada()]).size).to.equal(2);
});La tercera es interesante: no congelamos el aleatorio, porque queremos comprobar precisamente que hay variedad. Si necesitaras un código exacto, tendrías que stubear Math.random o, mejor, inyectar el generador.
Ejercicio 2
it('incluye precioGbp cuando el conversor responde', async () => {
const divisas = { obtenerTasa: sinon.stub().resolves(0.86) };
const repositorio = { buscarPorId: sinon.stub().resolves(crearEvento({ id: 'evt-001' })) };
const { req, res, next } = crearContexto({ params: { id: 'evt-001' }, query: { divisa: 'GBP' } });
await obtenerEvento({ repositorioEventos: repositorio, divisas })(req, res, next);
expect(res.codigo).to.equal(200);
expect(res.cuerpo).to.have.property('precioGbp');
});
it('omite precioGbp y sigue devolviendo 200 si el conversor falla', async () => {
const divisas = { obtenerTasa: sinon.stub().resolves(null) }; // proveedor caido
const repositorio = { buscarPorId: sinon.stub().resolves(crearEvento({ id: 'evt-001' })) };
const { req, res, next } = crearContexto({ params: { id: 'evt-001' }, query: { divisa: 'GBP' } });
await obtenerEvento({ repositorioEventos: repositorio, divisas })(req, res, next);
expect(res.codigo).to.equal(200);
expect(res.cuerpo).to.not.have.property('precioGbp');
expect(next.called).to.be.false;
});La segunda es la valiosa: garantiza que una caída del proveedor externo no rompe el catálogo. Esa clase de garantía solo se consigue con dobles.
Ejercicio 3
La prueba crea un stub que devuelve 4500 y comprueba que devuelve 4500: no ejecuta ni una línea del código de producción, así que pasaría aunque aplicarDescuento estuviera vacío. Correcto es usar la función real, que es pura y determinista:
expect(aplicarDescuento(5000, 'socio')).to.equal(4500);
expect(aplicarDescuento(5000, 'asistente')).to.equal(5000);
expect(aplicarDescuento(2555, 'socio')).to.equal(2299); // 2299.5 -> 2299La tercera aserción es la que de verdad importa: el redondeo de dinero es donde aparecen los céntimos fantasma que descuadran la contabilidad.
Conclusión
Ya sabemos aislar una unidad de todo lo que la hace impredecible. Conocemos la taxonomía de los dobles y cuándo usar cada uno, dominamos spy y stub con su repertorio, entendemos por qué sinon.restore() en afterEach no es opcional y cómo imponerlo con un gancho raíz, sabemos congelar y avanzar el tiempo para probar caducidades y reintentos sin esperar, hemos simulado fetch para cubrir el 500 y el tiempo agotado que jamás podríamos provocar contra el proveedor real, y hemos visto que un código que recibe sus dependencias se prueba con la mitad de esfuerzo que uno que las busca por su cuenta.
Y hemos visto el límite. Cada doble es una suposición sobre cómo se comporta la pieza real. Nuestras pruebas están verdes, pero ninguna ha comprobado todavía que las rutas estén montadas en el orden correcto, que manejadorDeErrores traduzca de verdad un ConflictoDeEstado en un 409, que la transacción del módulo 7 impida la sobreventa con diez compras a la vez, ni que autenticar rechace realmente una petición sin token.
En la lección siguiente, Pruebas de Integración, arrancamos la aplicación entera con supertest y la interrogamos por HTTP como lo haría un cliente real. Ahí se cobrará el diseño del módulo 6 —crearAplicacion() devuelve la aplicación sin llamar a listen, precisamente para esto— y resolveremos por fin la promesa del módulo 8: cómo consigue una prueba un token válido sin escribir una sola contraseña en el código.
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
