A la lliçó anterior vam provar el domini pur d'Escena Viva en cinquanta-dos mil·lisegons. Va ser fàcil perquè Sessio, Esdeveniment i politica.js no depenen de res: els dones dades, et retornen resultats. Tota l'aplicació real, en canvi, és plena de dependències que trenquen el determinisme.

  • src/serveis/canvi-divises.js crida una API externa amb fetch: si el proveïdor està caigut, la prova falla sense que el nostre codi tingui cap defecte, i si funciona triga 400 ms i consumeix quota.
  • generarCodiEntrada construeix codis EV-<any>-<6 dígits> amb l'any actual: aquesta prova passarà fins al 31 de desembre i fallarà l'1 de gener.
  • Els tokens d'accés caduquen als 15 minuts. Provar la caducitat tal qual requeriria una prova de quinze minuts.
  • Els repositoris obren connexions a Mongo. Una prova unitària d'un controlador no hauria de necessitar una base de dades.

La solució són els dobles de prova: objectes que substitueixen les dependències reals durant la prova, igual que un doble d'acció substitueix l'actor a l'escena perillosa. A Node l'eina estàndard és Sinon.

Contingut

  1. Taxonomia dels dobles de prova
  2. sinon.spy: observar sense canviar
  3. sinon.stub: substituir el comportament
  4. Restaurar sempre: caixes de sorra i contaminació
  5. Rellotges falsos
  6. Simular fetch per provar canvi-divises.js
  7. Simular el repositori per provar un controlador
  8. Injecció de dependències davant de pedaçar mòduls
  9. Quan NO fer servir dobles

Taxonomia dels dobles de prova

La terminologia ve de Gerard Meszaros i apareix a tota la literatura i als noms de l'API.

Doble Què és Quan fer-lo servir A Sinon
Dummy Un valor que es passa només per omplir un paràmetre; no es fa servir mai Un argument obligatori irrellevant per a la prova {}, null, () => {}
Stub Substitueix la dependència retornant respostes preprogramades Controlar què entra a la unitat sota prova sinon.stub()
Spy Embolcalla la funció real i registra com s'ha cridat, sense canviar-la Verificar què ha sortit de la unitat, mantenint el comportament sinon.spy()
Mock Stub amb expectatives declarades per endavant que es verifiquen al final Quan la interacció és el comportament que s'ha de provar sinon.mock()
Fake Implementació simplificada però funcional Un repositori en memòria, una cua de mentida Classe pròpia, sinon.fake()

La distinció pràctica que més faràs servir és stub davant d'spy: un spy et deixa preguntar s'ha cridat això, amb quins arguments i quantes vegades? sense alterar res; un stub a més decideix què retorna, i per tant controla el flux de la unitat sota prova. Regla d'or: stubs per a les entrades (allò que la unitat rep del món) i spies per a les sortides (allò que la unitat fa cap al món).

Evita els mock clàssics de Sinon: les seves expectatives per endavant produeixen proves fràgils i difícils de llegir. A la pràctica, stub més assercions al final cobreix tot el que necessites.

npm install --save-dev sinon

sinon.spy: observar sense canviar

Un espia embolcalla una funció i la deixa fer la seva feina, registrant cada crida.

// Funcio solta: el doble ES el callback
const alRegistrarVenda = sinon.spy();
gestor.on('venda-registrada', alRegistrarVenda);
gestor.registrarVenda(sessio, 2);
expect(alRegistrarVenda.calledOnce).to.be.true;

// Metode d'un objecte existent: mante el comportament real
const espiaVendre = sinon.spy(sessio, 'vendre');
sessio.vendre(3);
expect(espiaVendre.calledWith(3)).to.be.true;
expect(sessio.lliures).to.equal(297); // el metode real SI que s'ha executat
Propietat / mètode Què comprova
called, calledOnce, callCount S'ha cridat; una sola vegada; nombre exacte
calledWith(a, b) / calledWithExactly(a, b) Alguna crida ha rebut aquests arguments (comparació estructural), o exactament aquests i cap més
firstCall.args, lastCall.args, getCall(n).args Arguments d'una crida concreta
calledBefore(altre), threw(), returnValues Ordre relatiu entre espies; si ha llançat; què ha retornat

Un consell sobre els missatges de fallada: expect(espia.calledWith('compra')).to.be.true falla amb expected false to be true, que no diu res. Val més comparar els arguments (expect(espia.firstCall.args[0]).to.equal('compra'), que falla amb expected 'refresc' to equal 'compra') o fer servir les assercions pròpies de Sinon, que imprimeixen les crides registrades:

sinon.assert.calledWith(espia, 'compra', sinon.match({ usuariId: 'usr-001' }));

sinon.match permet assercions parcials molt útils quan l'argument té camps volàtils: sinon.match.string, sinon.match.number, sinon.match.instanceOf(Esdeveniment), sinon.match.has('codi', 'AFORAMENT_INSUFICIENT').

sinon.stub: substituir el comportament

Un stub reemplaça la implementació completament. El seu repertori:

const repo = { cercarPerId: () => {}, guardar: () => {} };

sinon.stub(repo, 'cercarPerId').returns(crearEsdeveniment());   // valor sincron
sinon.stub(repo, 'cercarPerId').resolves(crearEsdeveniment());  // promesa resolta
sinon.stub(repo, 'guardar').rejects(new ConflicteDEstat('Aforament insuficient'));
sinon.stub(repo, 'guardar').throws(new ErrorDeValidacio('Quantitat invalida'));

// Comportament diferent segons la crida: or pur per provar reintents
const cercar = sinon.stub();
cercar.onCall(0).rejects(new Error('ETIMEDOUT'));
cercar.onCall(1).rejects(new Error('ETIMEDOUT'));
cercar.onCall(2).resolves({ taxa: 1.09 });

// Comportament segons els arguments, amb cas per defecte
const esdeveniments = { cercarPerId: sinon.stub() };
esdeveniments.cercarPerId.withArgs('evt-001').resolves(crearEsdeveniment());
esdeveniments.cercarPerId.resolves(null);

sinon.stub(servei, 'calcular').callThrough();  // deixa passar a l'original
sinon.stub(repo, 'guardar').callsFake(async (c) => ({ ...c, id: 'ped-999' }));

Un stub també és un spy: conserva calledOnce, firstCall.args i tot el repertori anterior. Per això a la pràctica gairebé sempre faràs servir stub.

sinon.fake és una API més moderna i minimalista amb la mateixa idea (sinon.fake.returns(...), sinon.fake.resolves(...), sinon.fake.throws(...)). És immutable —no es reprograma un cop creada— i això la fa més predictible; funciona molt bé com a callback anònim, mentre que per substituir mètodes d'objectes existents stub continua sent el més còmode.

Restaurar sempre: caixes de sorra i contaminació

Aquí hi ha la fallada més difícil de diagnosticar de tot el mòdul. sinon.stub(objecte, 'metode') modifica l'objecte real en memòria, i a Node require desa els mòduls a la memòria cau: l'auditoria.js que veu la teva prova és exactament el mateix objecte que veuen totes les altres proves del procés. Si no el restaures, l'stub continua allà al fitxer següent.

it('registra la compra', () => {                 // test/unitat/compres.test.js
  sinon.stub(auditoria, 'registrar').resolves(); // sense restaurar
});
// A test/unitat/tokens.test.js, mes tard i al mateix proces,
// auditoria.registrar CONTINUA SENT L'STUB de l'altra prova.

Els símptomes són inconfusibles i desesperants: la prova passa sola però falla a la bateria completa, falla només quan s'executa després d'una altra de concreta, o apareix TypeError: Attempted to wrap registrar which is already wrapped.

La defensa és sinon.restore() a afterEach, que desfà tot el que s'ha creat amb la caixa de sorra per defecte (sinon.stub, sinon.spy, sinon.useFakeTimers). Quan vulguis un àmbit propi, sinon.createSandbox() et dona una caixa amb el seu propi caixa.restore(). I perquè ningú no se'n pugui oblidar, es posa en un ganxo arrel carregat des de .mocharc.json:

// test/ajudes/preparacio.js
'use strict';
const sinon = require('sinon');

// Ganxo arrel de Mocha: s'aplica a TOTES les proves del projecte
exports.mochaHooks = {
  afterEach() {
    sinon.restore();
  },
};
{ "spec": ["test/**/*.test.js"], "recursive": true, "timeout": 5000,
  "forbidOnly": true, "require": ["test/ajudes/preparacio.js"] }

Rellotges falsos

sinon.useFakeTimers() substitueix Date, setTimeout, setInterval, setImmediate i process.hrtime per versions controlades. El temps deixa de córrer sol: avança quan tu li ho dius amb clock.tick().

Cas 1: la caducitat d'un token d'accés de 15 minuts

describe('tokens d acces', () => {
  let rellotge;

  beforeEach(() => {
    rellotge = sinon.useFakeTimers(new Date('2026-10-01T10:00:00.000Z'));
  });

  afterEach(() => {
    rellotge.restore(); // IMPRESCINDIBLE: si no, la resta de proves viu al 2026-10-01
  });

  it('continua acceptant el token als 14 minuts', () => {
    const token = signarAcces({ id: 'usr-001', rol: 'assistent' });
    rellotge.tick(14 * 60 * 1000);
    expect(verificarAcces(token).id).to.equal('usr-001');
  });
  it('rebutja el token passats 15 minuts i un segon', () => {
    const token = signarAcces({ id: 'usr-001', rol: 'assistent' });
    rellotge.tick(15 * 60 * 1000 + 1000);

    let capturat = null;
    try {
      verificarAcces(token);
      expect.fail('S esperava ErrorDAutenticacio per token caducat');
    } catch (error) { capturat = error; }

    expect(capturat).to.be.instanceOf(ErrorDAutenticacio);
    expect(capturat.codi).to.equal('TOKEN_CADUCAT');
  });
});

Dues proves que cobreixen l'abans i el després de la caducitat, executades en menys d'un mil·lisegon. Sense el rellotge fals, la segona seria literalment impossible d'escriure. Funciona perquè jsonwebtoken fa servir Date.now() internament i el rellotge fals també afecta la signatura; si una biblioteca fes servir una font de temps que Sinon no intercepta, li hauries d'injectar el rellotge.

Cas 2: els reintents amb espera del mòdul 2

Provar reintentar(operacio, { intents: 3, esperaMs: 1000 }) de veritat costaria dos segons per prova. Amb el rellotge fals, zero:

it('reintenta fins a tres vegades amb espera exponencial', async () => {
  const rellotge = sinon.useFakeTimers();
  const operacio = sinon.stub();
  operacio.onCall(0).rejects(new Error('ETIMEDOUT'));
  operacio.onCall(1).rejects(new Error('ETIMEDOUT'));
  operacio.onCall(2).resolves({ taxa: 1.09 });

  const promesa = reintentar(operacio, { intents: 3, esperaMs: 1000 });
  await rellotge.tickAsync(1000);   // avanca el temps I deixa correr les microtasques
  await rellotge.tickAsync(2000);

  expect(await promesa).to.deep.equal({ taxa: 1.09 });
  expect(operacio.callCount).to.equal(3);
  rellotge.restore();
});

La clau és tickAsync en lloc de tick: tick avança els temporitzadors de manera síncrona, però entre dos reintents hi ha promeses pendents que necessiten que el bucle d'esdeveniments cedeixi el torn, i tickAsync ho permet. Advertiment repetit a propòsit: si no restaures el rellotge, totes les proves posteriors del procés viuen en un temps congelat, els timeouts de Mocha es comporten d'una manera estranyíssima i passaràs una tarda sencera sense entendre res. El ganxo arrel amb sinon.restore() també et cobreix aquí.

Simular fetch per provar canvi-divises.js

Aquest servei és l'exemple perfecte de dependència externa: crida per HTTP un proveïdor de taxes, amb AbortSignal.timeout, reintents i memòria cau en memòria. Volem provar-ne els comportaments sense tocar la xarxa.

/** Construeix una resposta semblant a la de fetch. */
function respostaFalsa({ estat = 200, cos = {} } = {}) {
  return { ok: estat >= 200 && estat < 300, status: estat, json: async () => cos };
}

describe('canvi de divises', () => {
  beforeEach(() => {
    buidarCache(); // la memoria cau es estat compartit entre proves: cal netejar-la
  });

  it('retorna la taxa quan el proveidor respon correctament', async () => {
    sinon.stub(global, 'fetch').resolves(
      respostaFalsa({ cos: { base: 'EUR', taxes: { GBP: 0.86 } } }));
    expect(await obtenirTaxa('GBP')).to.equal(0.86);
  });

  it('degrada elegantment retornant null si el proveidor respon 500', async () => {
    sinon.stub(global, 'fetch').resolves(respostaFalsa({ estat: 500 }));
    // La compra no s'ha de trencar perque el conversor estigui caigut:
    // el preu es mostra en euros i prou.
    expect(await obtenirTaxa('GBP')).to.be.null;
  });

  it('degrada elegantment si s exhaureix el temps d espera', async () => {
    const avortat = new Error('The operation was aborted');
    avortat.name = 'AbortError';
    sinon.stub(global, 'fetch').rejects(avortat);
    expect(await obtenirTaxa('GBP')).to.be.null;
  });
});

A aquestes s'hi afegeix la de la memòria cau (dues consultes seguides i expect(peticio.calledOnce).to.be.true). Quatre escenaris —dos d'ells, el 500 i el temps exhaurit, impossibles de reproduir a voluntat contra el proveïdor real— provats en mil·lisegons, sense xarxa, sense quota i sense intermitència.

Un detall d'higiene fàcil de passar per alt: el buidarCache() del beforeEach. La memòria cau del servei és estat compartit dins del mateix procés; sense buidar-la, la segona prova respondria des de la memòria cau de la primera, fetch no es cridaria i la fallada seria desconcertant. Tot mòdul amb estat en memòria necessita una funció de reinici pensada per a les proves, perquè Sinon no en pot saber res.

Simular el repositori per provar un controlador

Un controlador d'Express és una funció amb tres arguments: el podem provar sense base de dades i sense HTTP donant-li dobles.

/** Dobles minims de req, res i next. */
function crearContext({ params = {}, usuari = null } = {}) {
  const res = { codi: null, cos: null,
    status(c) { this.codi = c; return this; },
    json(c) { this.cos = c; return this; } };
  return { req: { params, usuari }, res, next: sinon.spy() };
}

it('respon 200 amb l esdeveniment serialitzat', async () => {
  const repositori = { cercarPerId: sinon.stub().resolves(crearEsdeveniment({ id: 'evt-001' })) };
  const { req, res, next } = crearContext({ params: { id: 'evt-001' } });
  await obtenirEsdeveniment({ repositoriEsdeveniments: repositori })(req, res, next);
  expect(repositori.cercarPerId.calledWith('evt-001')).to.be.true;
  expect(res.codi).to.equal(200);
  expect(next.called).to.be.false;
});

it('delega en next amb RecursNoTrobat si l esdeveniment no existeix', async () => {
  const repositori = { cercarPerId: sinon.stub().resolves(null) };
  const { req, res, next } = crearContext({ params: { id: 'evt-999' } });

  await obtenirEsdeveniment({ repositoriEsdeveniments: repositori })(req, res, next);

  expect(next.calledOnce).to.be.true;
  expect(next.firstCall.args[0]).to.be.instanceOf(RecursNoTrobat);
  expect(next.firstCall.args[0].codi).to.equal('ESDEVENIMENT_NO_TROBAT');
});

La segona prova comprova que el controlador delega l'error en next en lloc de respondre ell mateix. Aquest és el contracte que fa funcionar el gestorDErrors del mòdul 6, i és la mena de detall que es trenca quan algú «simplifica» el controlador.

Injecció de dependències davant de pedaçar mòduls

Deus haver notat la signatura obtenirEsdeveniment({ repositoriEsdeveniments })(req, res, next). No és casualitat: és una fàbrica de controladors que rep les seves dependències. Compara les dues maneres d'escriure el mateix.

// DIFICIL DE PROVAR: el controlador decideix d'on treu les dades
const { repositoriEsdeveniments } = require('../repositoris/index.js');
async function obtenirEsdeveniment(req, res, next) {
  const esdeveniment = await repositoriEsdeveniments.cercarPerId(req.params.id);
}

// FACIL DE PROVAR: fabrica que rep les seves dependencies
function obtenirEsdeveniment({ repositoriEsdeveniments }) {
  return async function gestor(req, res, next) {
    try {
      const esdeveniment = await repositoriEsdeveniments.cercarPerId(req.params.id);
      if (!esdeveniment) throw new RecursNoTrobat('Esdeveniment no trobat', {
        codi: 'ESDEVENIMENT_NO_TROBAT', detalls: { id: req.params.id } });
      res.status(200).json(esdeveniment.aJSON());
    } catch (error) { next(error); }
  };
}

A l'enrutador només canvia una línia: enrutador.get('/:id', obtenirEsdeveniment({ repositoriEsdeveniments })). Aquesta és una refactorització petita i honesta: el codi de producció es comporta igual, l'aplicació continua cablejant el repositori real una sola vegada, i a canvi el controlador esdevé trivialment provable. Aquest és el sentit de «les proves exerceixen pressió de disseny»: el codi difícil de provar sol estar massa acoblat, i arreglar-ho millora el codi, no només la prova.

Amb la versió rígida, a més, sinon.stub sobre l'objecte exportat pot no tenir efecte, perquè la desestructuració del require ja ha capturat la referència. Quan no puguis refactoritzar —codi de tercers, mòdul heretat— queda l'últim recurs, proxyquire, que intercepta els require d'un mòdul:

const { obtenirEsdeveniment } = proxyquire('../../src/controladors/esdeveniments.js', {
  '../repositoris/index.js': { repositoriEsdeveniments: { cercarPerId: sinon.stub().resolves(null) } },
});

Els seus inconvenients justifiquen que sigui l'últim recurs: acobla la prova a les rutes de require (mous un fitxer i es trenca sense que el comportament canviï), se salta la memòria cau de mòduls amb efectes estranys si hi ha estat, amaga el problema de disseny en comptes de resoldre'l, i no funciona amb ESM, així que és un carreró sense sortida si algun dia migres.

Quan NO fer servir dobles

Els dobles són tan còmodes que és fàcil passar-se, i una prova amb massa dobles només comprova les teves pròpies suposicions. Mira aquesta joia:

// PROVA INUTIL: no detecta cap fallada real
it('crea la comanda', async () => {
  const repositori = { crearComanda: sinon.stub().resolves({ totalCentims: 5000 }) };
  const servei = crearServeiDeComandes({ repositori });
  const comanda = await servei.comprarEntrades({ sessioId: 'ses-001-1', quantitat: 2 });
  expect(comanda.totalCentims).to.equal(5000);
});

Què prova, això? Que un stub que retorna 5000 retorna 5000. Si comprarEntrades calculés malament el total, passaria. Si no comprovés l'aforament, passaria. Si vengués entrades negatives, passaria. Costa de mantenir i no es pot posar vermella per una fallada real.

I hi ha un problema pitjor: el doble pot mentir sobre la dependència real. Si el teu stub del repositori retorna null quan no troba l'esdeveniment però el repositori real llança CastError amb un id malformat, les teves proves unitàries estaran totes verdes mentre l'API retorna 500 en producció. El doble codifica el que tu creus que fa la dependència.

Situació Doblar?
Xarxa, serveis externs, passarel·les de pagament Sí, sempre en proves unitàries
Rellotge, aleatorietat, identificadors generats Sí
Escenaris impossibles de provocar (500, timeout, disc ple) Sí, és l'única manera
Operacions deliberadament lentes (bcrypt cost 12 en bucle) Sí, amb criteri
Objectes de valor i del domini (Sessio, Esdeveniment) No, són barats i reals
Lògica pura del teu propi codi No, mai
La base de dades en una prova d'integració No, allà és justament el que vols provar

La regla que ho resumeix: dobla la vora del sistema, no el seu interior. I accepta que les proves unitàries amb dobles, per moltes que en siguin, no demostren que les peces encaixin. Les proves d'integració de la lliçó següent són el contrapès necessari; sense elles, els dobles es converteixen en un mirall on el teu codi s'admira a si mateix.

Errors Comuns i Consells

  • Oblidar sinon.restore(). Contaminació entre fitxers, already wrapped, proves que fallen només en bateria. Posa el ganxo arrel a test/ajudes/preparacio.js i oblida-te'n.
  • No restaurar el rellotge fals. Tot el procés queda congelat en el temps: la fallada més desconcertant que existeix.
  • Fer servir tick on cal tickAsync. Els reintents amb promeses no avancen i la prova mor per timeout.
  • Stub sobre una referència desestructurada. const { registrar } = require('./auditoria.js') captura la funció; sinon.stub(auditoria, 'registrar') canvia la propietat de l'objecte, no la teva còpia. Crida sempre a través de l'objecte.
  • expect(espia.calledWith(x)).to.be.true. Missatge de fallada inútil: fes servir sinon.assert.calledWith o compara firstCall.args.
  • Oblidar netejar l'estat en memòria (la memòria cau de canvi-divises.js). Sinon no ho sap; s'ha de fer a mà.
  • Simular el teu propi domini. Si estàs doblant Sessio, atura't: és barata, determinista i real.
  • Consell: quan un stub necessiti més de tres withArgs, planteja't un fake de veritat; una classe petita amb un Map a dins sol ser més llegible.
  • Consell: cada vegada que escriguis un stub, pregunta't «quina fallada real detectaria aquesta prova?». Si no saps respondre, sobra.

Exercicis

Exercici 1: l'any del codi d'entrada

generarCodiEntrada() produeix codis EV-<any>-<6 dígits> amb l'any actual i sis dígits aleatoris. Escriu proves deterministes que comprovin: que l'any del codi és l'any en curs congelant el rellotge a 2026-11-05, que el format compleix exactament el patró, i que dos codis generats seguits són diferents.

Exercici 2: la degradació del conversor al controlador

El controlador d'esdeveniments mostra el preu també en lliures quan l'usuari demana ?divisa=GBP. Escriu dues proves unitàries amb dobles: una en què obtenirTaxa resol 0.86 i la resposta inclou preuGbp, i una altra en què resol null i la resposta no inclou preuGbp però continua sent un 200 correcte.

Exercici 3: detectar la prova inútil

Explica per què aquesta prova no aporta res i reescriu-la perquè detecti una fallada real:

it('aplica el descompte de soci', () => {
  const calculadora = { aplicarDescompte: sinon.stub().returns(4500) };
  expect(calculadora.aplicarDescompte(5000, 'soci')).to.equal(4500);
});

Solucions

Exercici 1

beforeEach(() => sinon.useFakeTimers(new Date('2026-11-05T12:00:00.000Z')));

it('fa servir l any en curs al prefix del codi', () => {
  expect(generarCodiEntrada()).to.match(/^EV-2026-/);
});

it('compleix el format EV-<any>-<6 digits>', () => {
  expect(generarCodiEntrada()).to.match(/^EV-\d{4}-\d{6}$/);
});

it('genera codis diferents en crides consecutives', () => {
  expect(new Set([generarCodiEntrada(), generarCodiEntrada()]).size).to.equal(2);
});

La tercera és interessant: no congelem l'aleatori, perquè volem comprovar precisament que hi ha varietat. Si necessitessis un codi exacte, hauries de fer un stub de Math.random o, millor, injectar el generador.

Exercici 2

it('inclou preuGbp quan el conversor respon', async () => {
  const divises = { obtenirTaxa: sinon.stub().resolves(0.86) };
  const repositori = { cercarPerId: sinon.stub().resolves(crearEsdeveniment({ id: 'evt-001' })) };
  const { req, res, next } = crearContext({ params: { id: 'evt-001' }, query: { divisa: 'GBP' } });

  await obtenirEsdeveniment({ repositoriEsdeveniments: repositori, divises })(req, res, next);

  expect(res.codi).to.equal(200);
  expect(res.cos).to.have.property('preuGbp');
});

it('omet preuGbp i continua retornant 200 si el conversor falla', async () => {
  const divises = { obtenirTaxa: sinon.stub().resolves(null) };  // proveidor caigut
  const repositori = { cercarPerId: sinon.stub().resolves(crearEsdeveniment({ id: 'evt-001' })) };
  const { req, res, next } = crearContext({ params: { id: 'evt-001' }, query: { divisa: 'GBP' } });
  await obtenirEsdeveniment({ repositoriEsdeveniments: repositori, divises })(req, res, next);
  expect(res.codi).to.equal(200);
  expect(res.cos).to.not.have.property('preuGbp');
  expect(next.called).to.be.false;
});

La segona és la valuosa: garanteix que una caiguda del proveïdor extern no trenca el catàleg. Aquesta mena de garantia només s'aconsegueix amb dobles.

Exercici 3

La prova crea un stub que retorna 4500 i comprova que retorna 4500: no executa ni una línia del codi de producció, així que passaria encara que aplicarDescompte estigués buida. El correcte és fer servir la funció real, que és pura i determinista:

expect(aplicarDescompte(5000, 'soci')).to.equal(4500);
expect(aplicarDescompte(5000, 'assistent')).to.equal(5000);
expect(aplicarDescompte(2555, 'soci')).to.equal(2299);  // 2299.5 -> 2299

La tercera asserció és la que de veritat importa: l'arrodoniment de diners és on apareixen els cèntims fantasma que desquadren la comptabilitat.

Conclusió

Ja sabem aïllar una unitat de tot allò que la fa impredictible. Coneixem la taxonomia dels dobles i quan fer servir cadascun, dominem spy i stub amb el seu repertori, entenem per què sinon.restore() a afterEach no és opcional i com imposar-ho amb un ganxo arrel, sabem congelar i avançar el temps per provar caducitats i reintents sense esperar, hem simulat fetch per cobrir el 500 i el temps exhaurit que mai no podríem provocar contra el proveïdor real, i hem vist que un codi que rep les seves dependències es prova amb la meitat d'esforç que un que se les busca pel seu compte.

I hem vist el límit. Cada doble és una suposició sobre com es comporta la peça real. Les nostres proves són verdes, però cap no ha comprovat encara que les rutes estiguin muntades en l'ordre correcte, que el gestorDErrors tradueixi de veritat un ConflicteDEstat en un 409, que la transacció del mòdul 7 impedeixi la sobrevenda amb deu compres alhora, ni que autenticar rebutgi realment una petició sense token.

A la lliçó següent, Proves d'Integració, arrenquem l'aplicació sencera amb supertest i la interroguem per HTTP com ho faria un client real. Allà es cobrarà el disseny del mòdul 6 —crearAplicacio() retorna l'aplicació sense cridar listen, precisament per això— i resoldrem per fi la promesa del mòdul 8: com aconsegueix una prova un token vàlid sense escriure ni una sola contrasenya al codi.

Curs de Node.js: De Principiant a Avançat

Mòdul 1: Introducció a Node.js

Mòdul 2: Conceptes Bàsics

Mòdul 3: Sistema de Fitxers i E/S

Mòdul 4: HTTP i Servidors Web

Mòdul 5: NPM i Gestió de Paquets

Mòdul 6: Framework Express.js

Mòdul 7: Bases de Dades i ORMs

Mòdul 8: Autenticació i Autorització

Mòdul 9: Proves i Depuració

Mòdul 10: Temes Avançats

Mòdul 11: Desplegament i DevOps

Mòdul 12: Projectes del Món Real

© Copyright 2026. Tots els drets reservats