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.js llama a una API externa con fetch: 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.
  • generarCodigoEntrada construye códigos EV-<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

  1. Taxonomía de los dobles de prueba
  2. sinon.spy: observar sin cambiar
  3. sinon.stub: sustituir el comportamiento
  4. Restaurar siempre: sandboxes y contaminación
  5. Relojes falsos
  6. Simular fetch para probar cambio-divisas.js
  7. Simular el repositorio para probar un controlador
  8. Inyección de dependencias frente a parcheo de módulos
  9. 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.

npm install --save-dev sinon

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.assert.calledWith(espia, 'compra', sinon.match({ usuarioId: 'usr-001' }));

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 en test/ayudas/preparacion.js y olvídate.
  • No restaurar el reloj falso. Todo el proceso queda congelado en el tiempo: el fallo más desconcertante que existe.
  • Usar tick donde hace falta tickAsync. 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: usa sinon.assert.calledWith o compara firstCall.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 un Map dentro 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 -> 2299

La 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

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