Cuatro módulos llevamos con un npm test que falla a propósito. Hoy se acaba. Instalamos Mocha y Chai, configuramos el ejecutor, arreglamos el script test del package.json y escribimos la batería unitaria del dominio de Escena Viva: Sesion, Evento, GestorDeVentas y —la promesa pendiente desde el módulo 8— la matriz entera de src/autorizacion/politica.js recorrida celda por celda.

Contenido

  1. Instalar la pila y arreglar el script test
  2. Anatomía de un fichero de pruebas
  3. Los ganchos y su orden de ejecución exacto
  4. Enfocar y saltar pruebas
  5. Chai: estilos y catálogo de aserciones
  6. Aserciones sobre errores
  7. Pruebas asíncronas y timeouts
  8. La batería unitaria de Escena Viva
  9. Fábricas de datos de prueba

Instalar la pila y arreglar el script test

npm install --save-dev mocha chai@4

La versión importa: Chai 5 solo publica ESM y no se carga con require(). Como Escena Viva es CommonJS, fijamos la 4; si ves "chai": "^5..." tendrás ERR_REQUIRE_ESM en la primera ejecución.

Las opciones del ejecutor van en .mocharc.json, en la raíz, para que sean visibles al equipo y al servidor de integración continua:

{
  "spec": ["test/**/*.test.js"],
  "recursive": true,
  "timeout": 5000,
  "reporter": "spec",
  "forbidOnly": true,
  "exit": false
}
Opción Significado
spec Patrón de ficheros; con la convención *.test.js no hay ambigüedad
recursive Baja por test/unidad/ y test/integracion/
timeout Milisegundos máximos por prueba (2000 por defecto)
reporter spec imprime la jerarquía describe/it de forma legible
forbidOnly Falla la ejecución si encuentra un .only olvidado
exit false: no fuerces la salida. Si algo queda abierto, quiero enterarme

La última merece comentario. Si Mocha termina pero el proceso no muere, es porque algo sigue vivo: una conexión a Mongo, un setInterval, un servidor escuchando. "exit": true esconde el problema; false te obliga a cerrar bien los recursos. En 09-06 veremos cómo diagnosticar qué quedó abierto.

Y ahora, por fin, el script que lleva desde el módulo 5 fallando. Junto a los que ya existían (start, dev, semilla, lint, format, auditoria), añadimos:

{ "test": "mocha",
  "test:unidad": "mocha test/unidad/**/*.test.js",
  "test:ver": "mocha --watch",
  "comprobar": "npm run lint && npm test" }

Ya no hace falta pasar rutas: Mocha lee .mocharc.json. test:ver re-ejecuta al guardar, que es la forma de trabajar mientras desarrollas. npm test responde ahora 0 passing con código de salida 0: el primer verde del proyecto.

Anatomía de un fichero de pruebas

Mocha expone globales (describe, it, before...) sin importarlas; Chai sí se importa.

// test/unidad/sesion.test.js
'use strict';

const { expect } = require('chai');
const { Sesion } = require('../../src/dominio/sesion.js');

describe('Sesion', () => {
  describe('vender()', () => {
    it('descuenta las localidades vendidas del aforo disponible', () => {
      const sesion = new Sesion({          // preparar
        id: 'ses-001-1', eventoId: 'evt-001', inicio: '2026-10-17T20:00:00.000Z',
        aforo: 400, vendidas: 100, precioCentimos: 2500,
      });
      sesion.vender(3);                    // actuar
      expect(sesion.libres).to.equal(297); // comprobar
    });
  });
});

describe agrupa y se anida libremente: por convención el exterior nombra la unidad y los interiores el método o el escenario. it es una prueba: pasa si la función termina sin lanzar, falla si lanza (y una aserción de Chai incumplida lanza). Regla de estilo: no uses funciones flecha si necesitas el this de Mocha (por ejemplo this.timeout(10000)), porque las flechas no tienen this propio.

Los ganchos y su orden de ejecución exacto

Gancho Cuándo Para qué
before Una vez, antes de todas las pruebas del bloque Conectar a la base de datos, recursos caros
beforeEach Antes de cada it del bloque y de los anidados Recrear el estado limpio de cada prueba
afterEach Después de cada it Restaurar dobles, limpiar datos
after Una vez, al final del bloque Cerrar conexiones

Con un describe exterior que tiene los cuatro ganchos y la prueba A, y un describe interior con sus cuatro ganchos y las pruebas B y C, la traza real es exactamente esta:

before exterior
beforeEach exterior  >> prueba A  afterEach exterior
before interior
beforeEach exterior · beforeEach interior  >> prueba B  afterEach interior · afterEach exterior
beforeEach exterior · beforeEach interior  >> prueba C  afterEach interior · afterEach exterior
after interior · after exterior

Las tres reglas que se deducen: los beforeEach corren de fuera hacia dentro y los afterEach de dentro hacia fuera, como una pila; el beforeEach exterior corre también para las pruebas de los bloques interiores (fuente habitual del "¿por qué se reinicia mi objeto?"); y el before interior no corre hasta la primera prueba de ese bloque.

Qué poner en cada uno: en before, solo lo caro e inmutable, nunca estado que las pruebas vayan a mutar. En beforeEach, la preparación, porque si dos pruebas comparten un objeto mutable tarde o temprano se pisarán. En afterEach, la limpieza obligatoria (sinon.restore(), borrar colecciones, restaurar relojes): Mocha lo ejecuta aunque la prueba falle, y por eso es el sitio correcto. En after, cerrar lo que abrió before, o el proceso no terminará.

Enfocar y saltar pruebas

it.only('descuenta las localidades vendidas', () => { /* ... */ });  // solo esta
it.skip('agrupa varias sesiones en un pedido', () => { /* saltada */ });
it('emite recordatorio 24h antes de la sesion');  // pendiente: sin función

El error clásico con .only es olvidarlo, subirlo y que CI pase en verde ejecutando una sola prueba. Por eso pusimos forbidOnly: true: un .only olvidado rompe CI de forma ruidosa en vez de mentir en silencio (en local se desactiva con mocha --forbid-only=false).

Las pruebas pendientes son una forma honesta de dejar escrito el comportamiento que falta: aparecen en el informe, así que no se olvidan como un TODO. Existe además el salto condicional, before(function () { if (!process.env.MONGODB_URI_TEST) this.skip(); }), útil cuando una prueba requiere un servicio que puede no estar.

Chai: estilos y catálogo de aserciones

Chai ofrece tres estilos para decir lo mismo: assert.equal(a, b), expect(a).to.equal(b) y a.should.equal(b). Usaremos expect porque se lee casi como una frase, no modifica Object.prototype (a diferencia de should, que revienta con null y undefined) y sus mensajes de fallo incluyen el valor esperado y el obtenido.

Aserción Qué comprueba Ejemplo
to.equal(v) Igualdad estricta (===) expect(sesion.libres).to.equal(297)
to.deep.equal(v) Igualdad estructural expect(pedido.lineas).to.deep.equal([{ cantidad: 2 }])
to.include(v) Contiene elemento, subcadena o subconjunto expect(codigo).to.include('EV-2026-')
to.have.property(p, v) Existe la propiedad, opcionalmente con valor expect(cuerpo.error).to.have.property('codigo', 'AFORO_INSUFICIENTE')
to.have.lengthOf(n) Longitud de array o cadena expect(evento.sesiones).to.have.lengthOf(2)
to.be.true / to.be.false / to.be.null Booleano exacto y ausencia de valor expect(sesion.agotada).to.be.true
to.throw(Tipo, /regex/) La función lanza expect(() => sesion.vender(0)).to.throw(ErrorDeValidacion)
to.be.closeTo(v, delta) Números con tolerancia expect(sesion.ocupacion).to.be.closeTo(0.25, 0.001)
to.be.an('array') / to.be.instanceOf(C) Tipo e instancia expect(catalogo).to.be.an('array')

El error número uno: equal frente a deep.equal

const obtenido = { id: 'evt-001', titulo: 'Concierto de Otono' };

expect(obtenido).to.equal({ id: 'evt-001', titulo: 'Concierto de Otono' });
// FALLA: son dos objetos distintos en memoria, === es false

expect(obtenido).to.deep.equal({ id: 'evt-001', titulo: 'Concierto de Otono' });
// PASA: compara estructura, clave por clave, recursivamente

expect(obtenido).to.deep.include({ titulo: 'Concierto de Otono' });
// Solo las claves que importan: ideal con campos volátiles como creadoEn

Regla mnemotécnica: equal para primitivos, deep.equal para todo lo que se escriba con llaves o corchetes. Cuando veas expected { id: 'evt-001' } to equal { id: 'evt-001' } con dos objetos aparentemente idénticos, te falta el deep.

Aserciones sobre errores

Con la jerarquía del módulo 6, comprobar solo que "algo lanzó" es insuficiente: si vender(0) lanzara un TypeError interno en lugar del ErrorDeValidacion esperado, una prueba floja pasaría igual. La escala va de to.throw() a secas (floja), a to.throw(ErrorDeValidacion) (mejor), a to.throw(ErrorDeValidacion, /entero/) (tipo y mensaje).

Pero lo que de verdad viaja al cliente es el codigo, del que depende el estado HTTP a través de ESTADO_POR_CODIGO. El patrón de captura permite afirmar sobre él:

it('lanza ConflictoDeEstado con codigo AFORO_INSUFICIENTE si no quedan localidades', () => {
  const sesion = crearSesion({ aforo: 400, vendidas: 398 });  // solo 2 libres
  let capturado = null;
  try {
    sesion.vender(5);
    expect.fail('Se esperaba que vender lanzara ConflictoDeEstado');
  } catch (error) { capturado = error; }

  expect(capturado).to.be.instanceOf(ConflictoDeEstado);
  expect(capturado.codigo).to.equal('AFORO_INSUFICIENTE');
  expect(capturado.detalles).to.deep.include({ libres: 2, solicitadas: 5 });
});

El expect.fail(...) es imprescindible: sin él, si vender(5) no lanzara, el catch no correría, capturado seguiría siendo null y las aserciones fallarían con un mensaje confuso. Cuando basta una propiedad hay versión compacta: expect(() => sesion.vender(5)).to.throw(ConflictoDeEstado).with.property('codigo', 'AFORO_INSUFICIENTE').

Pruebas asíncronas y timeouts

Mocha ofrece tres mecanismos: async/await en el it (la forma correcta), devolver la promesa (equivalente) y el callback done, herencia de la era pre-promesas que sigue siendo la mejor opción para eventos que se emiten en el futuro.

it('devuelve el evento con sus sesiones', async () => {
  const evento = await repositorioEventos.buscarPorId('evt-001');
  expect(evento.sesiones).to.have.lengthOf(2);
});

Verás done en acción unas páginas más abajo, con GestorDeVentas. Sus trampas: si nunca lo llamas la prueba muere por timeout con un mensaje genérico; si una aserción falla dentro del callback puede no atribuirse a la prueba correcta; y nunca lo mezcles con async (Mocha lanza Resolution method is overspecified).

El fallo silencioso: la promesa no esperada

// MAL: el it retorna undefined; la aserción se ejecuta DESPUÉS de que la prueba terminó.
it('lanza si el evento no existe', () => {
  (async () => {
    const evento = await repositorioEventos.buscarPorId('evt-999');
    expect(evento).to.be.null;
  })();
});

Mocha da el it por bueno de inmediato y la aserción se evalúa en el vacío: si falla, aparecerá como un unhandledRejection sin relación aparente con la prueba, o directamente no aparecerá. Regla: si en el cuerpo de un it aparece async o .then, el it tiene que ser async y esperar.

Para probar que una promesa se rechaza sirve el mismo try/catch con expect.fail, o el complemento chai-as-promised (npm i -D chai-as-promised@7 y chai.use(chaiComoPromesas)), que añade await expect(promesa).to.be.rejectedWith(RecursoNoEncontrado). Ojo al await delante de expect: sin él vuelves a tener una promesa no esperada y la prueba pasa siempre. Es la misma trampa disfrazada.

Timeouts

El mensaje Error: Timeout of 2000ms exceeded significa una de tres cosas, por frecuencia: olvidaste done() o devolviste una promesa que nunca se resuelve; la operación es realmente lenta (base de datos, bcrypt con coste 12); o hay un interbloqueo esperando un evento que nadie emite. Se ajusta por bloque (describe('...', function () { this.timeout(10000); })), por prueba, o se desactiva con this.timeout(0) mientras depuras. No subas el timeout global para tapar una prueba lenta: convertirías un interbloqueo de dos segundos en una espera de treinta.

La batería unitaria de Escena Viva

test/unidad/sesion.test.js

'use strict';

const { expect } = require('chai');
const { ErrorDeValidacion, ConflictoDeEstado } = require('../../src/errores.js');
const { crearSesion } = require('../ayudas/fabricas.js');

describe('Sesion', () => {
  describe('vender()', () => {
    it('descuenta las localidades vendidas del aforo disponible', () => {
      const sesion = crearSesion({ aforo: 400, vendidas: 100 });
      sesion.vender(3);
      expect(sesion.libres).to.equal(297);
    });

    it('lanza ErrorDeValidacion si la cantidad no es un entero positivo', () => {
      const sesion = crearSesion({ aforo: 400, vendidas: 0 });
      [0, -2, 1.5, 'dos'].forEach((c) => expect(() => sesion.vender(c))
        .to.throw(ErrorDeValidacion));
    });

    it('lanza ConflictoDeEstado con codigo AFORO_INSUFICIENTE si no caben', () => {
      const sesion = crearSesion({ aforo: 400, vendidas: 398 });
      expect(() => sesion.vender(5)).to.throw(ConflictoDeEstado)
        .with.property('codigo', 'AFORO_INSUFICIENTE');
    });

    it('permite vender exactamente las ultimas localidades libres', () => {
      const sesion = crearSesion({ aforo: 400, vendidas: 397 });
      sesion.vender(3);
      expect(sesion.libres).to.equal(0);
      expect(sesion.agotada).to.be.true;
    });
  });
});

Faltarían las propiedades derivadas (ocupacion con be.closeTo, precioEuros, agotada en falso), que siguen el mismo molde. El caso frontera —vender exactamente las últimas tres— es el <= contra < que un día se escribe mal y provoca una sobreventa de una entrada.

test/unidad/evento.test.js

Evento es un agregado, así que sus pruebas van sobre las sumas y sobre la reconstrucción: aforoTotal es 800 y vendidasTotales 350 con dos sesiones de 400 y 100/250 vendidas; agotado es false si una sola sesión aún tiene sitio; y Evento.desdeJSON({ id, titulo, salaId, sesiones }) devuelve una instanceOf(Evento) cuyas sesiones son instancias de Sesion con su libres ya calculado, además de lanzar si el JSON no trae identificador. Es el mismo patrón que en Sesion: escenario mínimo, una operación, aserción sobre el valor observable.

test/unidad/politica.test.js — la matriz celda por celda

La prueba prometida en el módulo 8. Como tienePermiso, puedeGestionarEvento y puedeVerPedido son funciones puras, recorremos la matriz completa con tablas de casos y bucles:

describe('politica de autorizacion', () => {
  describe('tienePermiso()', () => {
    const CASOS = [
      ['asistente', PERMISOS.VER_CATALOGO, true],
      ['asistente', PERMISOS.COMPRAR_ENTRADAS, true],
      ['asistente', PERMISOS.GESTIONAR_EVENTOS, false],
      ['asistente', PERMISOS.GESTIONAR_USUARIOS, false],
      ['organizador', PERMISOS.GESTIONAR_EVENTOS, true],
      ['organizador', PERMISOS.VER_INFORMES_SALA, true],
      ['organizador', PERMISOS.GESTIONAR_USUARIOS, false],
      ['administrador', PERMISOS.GESTIONAR_EVENTOS, true],
      ['administrador', PERMISOS.GESTIONAR_USUARIOS, true],
      ['administrador', PERMISOS.VER_INFORMES_SALA, true],
      ['taquillero', PERMISOS.VER_CATALOGO, false],  // rol desconocido y sin rol:
      [undefined, PERMISOS.VER_CATALOGO, false],     // por ahi se cuelan las brechas
    ];

    CASOS.forEach(([rol, permiso, esperado]) => {
      it(`${esperado ? 'concede' : 'deniega'} ${permiso} al rol ${rol}`, () => {
        expect(tienePermiso(rol, permiso)).to.equal(esperado);
      });
    });
  });

  describe('puedeGestionarEvento()', () => {
    const evento = { id: 'evt-001', salaId: 'org-almendra' };  // del Teatro Almendra
    const PROPIEDAD = [
      ['administrador sobre cualquier evento', { rol: 'administrador', salaId: null }, true],
      ['organizador sobre su propia sala', { rol: 'organizador', salaId: 'org-almendra' }, true],
      ['organizador sobre otra sala', { rol: 'organizador', salaId: 'org-boveda' }, false],
      ['asistente sobre cualquier evento', { rol: 'asistente', salaId: null }, false],
      ['usuario nulo', null, false],  // el caso que mas veces se olvida
    ];

    PROPIEDAD.forEach(([caso, usuario, esperado]) => {
      it(`${esperado ? 'permite' : 'deniega'} gestionar: ${caso}`, () => {
        expect(puedeGestionarEvento(usuario, evento)).to.equal(esperado);
      });
    });
  });

  describe('puedeVerPedido()', () => {
    const pedido = { id: 'ped-001', usuarioId: 'usr-001' };

    it('permite al asistente ver su propio pedido', () => {
      expect(puedeVerPedido({ id: 'usr-001', rol: 'asistente' }, pedido)).to.be.true;
    });

    it('deniega al asistente ver el pedido de otro asistente', () => {
      expect(puedeVerPedido({ id: 'usr-002', rol: 'asistente' }, pedido)).to.be.false;
    });
  });
});

Diecisiete celdas recorridas con dos bucles de tres líneas, casos límite incluidos. La salida de Mocha lee como una especificación de seguridad, y si alguien invierte un if en puedeGestionarEvento, varias pruebas se ponen rojas nombrando exactamente el problema.

test/unidad/gestor-ventas.test.js

GestorDeVentas es un EventEmitter, así que probamos que emite lo que debe:

it('emite venta-registrada con la sesion y la cantidad vendida', (done) => {
  gestor.once('venta-registrada', (dato) => {
    expect(dato.sesionId).to.equal('ses-001-1');
    expect(dato.cantidad).to.equal(2);
    done();
  });
  gestor.registrarVenta(crearSesion({ id: 'ses-001-1', vendidas: 100 }), 2);
});

it('no repite aforo-bajo en ventas posteriores de la misma sesion', () => {
  const sesion = crearSesion({ aforo: 100, vendidas: 100 - UMBRAL_AFORO_BAJO - 1 });
  let veces = 0;
  gestor.on('aforo-bajo', () => { veces += 1; });
  gestor.registrarVenta(sesion, 2);
  gestor.registrarVenta(sesion, 1);
  expect(veces).to.equal(1);
});

Con un beforeEach(() => { gestor = new GestorDeVentas(); }) delante y una tercera prueba análoga para sesion-agotada. Conviven dos técnicas: done para el evento asíncrono y recolectar en una variable para los síncronos, que permite contar cuántas veces se emitió. La segunda es la clase de regla que nadie recuerda al refactorizar seis meses después.

Fábricas de datos de prueba

Repetir el constructor completo en cada prueba es ruido: oculta qué dato es relevante para cada caso. La solución son fábricas con valores por defecto y sobreescritura parcial:

// test/ayudas/fabricas.js
'use strict';

const { Sesion } = require('../../src/dominio/sesion.js');
const { Evento } = require('../../src/dominio/evento.js');

let contador = 0;

function crearSesion(sobreescrituras = {}) {
  contador += 1;
  return new Sesion({  // valores por defecto válidos, todo sobreescribible
    id: `ses-test-${contador}`, eventoId: 'evt-001',
    inicio: '2026-10-17T20:00:00.000Z',
    aforo: 400, vendidas: 0, precioCentimos: 2500, ...sobreescrituras,
  });
}

// crearEvento hace lo mismo con el Concierto de Otono de org-almendra,
// mapeando sus sesiones con crearSesion; crearUsuario devuelve a Lucia,
// asistente, [email protected], salaId nulo. Todo sobreescribible.

module.exports = { crearSesion, crearEvento, crearUsuario };

Tres propiedades hacen buena a una fábrica: devuelve siempre un objeto nuevo (nada de constantes compartidas), sus valores por defecto son válidos (crearSesion() sin argumentos produce una sesión utilizable) y la sobreescritura hace visible lo relevante (crearSesion({ vendidas: 398 }) dice sin ruido que esta prueba trata del aforo casi lleno).

Ejecuta ahora npm test: 38 passing (52 ms). Esa es la velocidad de las pruebas unitarias sobre dominio puro, y la razón de que la base de la pirámide sea tan ancha.

Errores Comunes y Consejos

  • ERR_REQUIRE_ESM al importar Chai. Estás en Chai 5 con CommonJS. Instala chai@4.
  • Usar equal con objetos. El fallo muestra dos objetos idénticos y te vuelve loco: necesitas deep.equal.
  • Resolution method is overspecified. Has puesto done y async en el mismo it. Elige uno.
  • Un .only olvidado. CI pasa en verde ejecutando una prueba. Activa forbidOnly.
  • this.timeout() en una función flecha. No funciona: usa function ().
  • Preparar en before en vez de en beforeEach. La primera prueba muta el objeto y las siguientes lo reciben sucio. Si dudas, beforeEach.
  • Aserciones que no se ejecutan. Toda prueba con try/catch necesita su expect.fail en el camino feliz.
  • Consejo: verifica cada prueba nueva rompiendo el código a propósito (cambia >= por > en vender y comprueba que se pone roja), y lanza un solo fichero con npx mocha test/unidad/sesion.test.js en vez de tocar .only.

Ejercicios

Ejercicio 1: precio de un pedido

Escribe test/unidad/pedido.test.js con cuatro pruebas para Pedido: que el total suma las líneas en céntimos, que un pedido recién creado está en estado pendiente, que no admite más de 6 entradas por sesión y que un pedido sin líneas lanza. Usa una fábrica local.

Ejercicio 2: la celda que falta

En la tabla CASOS falta PERMISOS.CANCELAR_PEDIDO. Añádelo para los tres roles con esta regla: el asistente puede cancelar sus pedidos, el organizador no, el administrador sí. Después escribe la prueba de puedeCambiarRol que verifique que solo el administrador puede cambiar roles y que nadie puede cambiarse el rol a sí mismo.

Ejercicio 3: cazar el falso positivo

Explica por qué esta prueba pasa siempre, incluso si buscarPorId devuelve null, y reescríbela:

it('encuentra el evento por id', () => {
  repositorioEventos.buscarPorId('evt-001').then((evento) => {
    expect(evento.titulo).to.equal('Concierto de Otono');
  });
});

Soluciones

Ejercicio 1. Con una fábrica local crearPedido(s) que por defecto trae una línea de 2 entradas a 2500 céntimos:

it('suma el total de todas sus lineas en centimos', () => {
  const pedido = crearPedido({ lineas: [
    { sesionId: 'ses-001-1', cantidad: 2, precioCentimos: 2500 },
    { sesionId: 'ses-002-1', cantidad: 1, precioCentimos: 1800 },
  ] });
  expect(pedido.totalCentimos).to.equal(6800);
});

Las otras tres son directas: crearPedido().estado debe ser 'pendiente' con pagadoEn nulo; una línea con cantidad: 7 debe lanzar ErrorDeValidacion con mensaje que incluya el 6; y crearPedido({ lineas: [] }) debe lanzar también.

Ejercicio 2. Tres filas nuevas en CASOS (['asistente', PERMISOS.CANCELAR_PEDIDO, true], ['organizador', ..., false], ['administrador', ..., true]) y cuatro pruebas de puedeCambiarRol con admin, marta (organizador) y lucia (asistente):

expect(puedeCambiarRol(admin, lucia)).to.be.true;   // el administrador si
expect(puedeCambiarRol(marta, lucia)).to.be.false;  // el organizador no
expect(puedeCambiarRol(lucia, marta)).to.be.false;  // el asistente no
expect(puedeCambiarRol(admin, admin)).to.be.false;  // nadie sobre si mismo

La última es la importante: impide que el último administrador se degrade por error y deje la plataforma sin nadie que pueda gestionar usuarios.

Ejercicio 3. El it no es async y no devuelve la promesa del .then, así que Mocha da la prueba por terminada antes de que el .then se ejecute; si la aserción falla después, el fallo llega huérfano. La versión correcta es async, con await y un expect(evento).to.not.be.null previo que da un mensaje de fallo mucho más claro que un Cannot read properties of null.

Conclusión

npm test está verde. Tenemos Mocha configurado con .mocharc.json, Chai con el estilo expect, los ganchos con su orden entendido de verdad, el catálogo de aserciones que se usa a diario, la técnica para comprobar que un error es el error correcto con su código, las formas de probar código asíncrono con su trampa de la promesa no esperada, y una batería real que cubre el dominio y la matriz de permisos completa.

Pero fíjate en lo que todas estas pruebas comparten: ninguna de sus unidades depende de nada externo. Sesion no llama a la red, politica.js no consulta la base de datos, GestorDeVentas no mira el reloj. Por eso se prueban en milisegundos y de forma determinista.

En cuanto salgamos de esa burbuja, el terreno cambia. cambio-divisas.js llama a una API por fetch, generarCodigoEntrada usa el año actual, los tokens de acceso caducan a los 15 minutos y los repositorios abren conexiones a Mongo. Ninguna de esas unidades se puede probar de forma determinista tal cual está.

En la lección siguiente, Dobles de Prueba con Sinon, sustituiremos el reloj, la red y la base de datos por versiones controladas: espías, stubs, mocks y relojes falsos. Y veremos también dónde está el límite, porque simular demasiado produce pruebas que solo comprueban tus propias suposiciones.

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