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
- Instalar la pila y arreglar el script
test - Anatomía de un fichero de pruebas
- Los ganchos y su orden de ejecución exacto
- Enfocar y saltar pruebas
- Chai: estilos y catálogo de aserciones
- Aserciones sobre errores
- Pruebas asíncronas y timeouts
- La batería unitaria de Escena Viva
- Fábricas de datos de prueba
Instalar la pila y arreglar el script test
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 exteriorLas 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ónEl 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 creadoEnRegla 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_ESMal importar Chai. Estás en Chai 5 con CommonJS. Instalachai@4.- Usar
equalcon objetos. El fallo muestra dos objetos idénticos y te vuelve loco: necesitasdeep.equal. Resolution method is overspecified. Has puestodoneyasyncen el mismoit. Elige uno.- Un
.onlyolvidado. CI pasa en verde ejecutando una prueba. ActivaforbidOnly. this.timeout()en una función flecha. No funciona: usafunction ().- Preparar en
beforeen vez de enbeforeEach. La primera prueba muta el objeto y las siguientes lo reciben sucio. Si dudas,beforeEach. - Aserciones que no se ejecutan. Toda prueba con
try/catchnecesita suexpect.failen el camino feliz. - Consejo: verifica cada prueba nueva rompiendo el código a propósito (cambia
>=por>envendery comprueba que se pone roja), y lanza un solo fichero connpx mocha test/unidad/sesion.test.jsen 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 mismoLa ú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
- ¿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
