La lliçó anterior va acabar amb una frontera clara: ESLint sap dir-te que una variable no es fa servir, però no sap que el resum del backlog ha de donar 45 hores obertes de 48. Això és comportament, i el comportament només es comprova d'una manera: executant el codi amb entrades conegudes i comparant la sortida amb l'esperada. Automatitzat, repetible, en segons. Aquesta lliçó és on el Mòdul 8 salda els seus deutes: les tres proves que vas deixar apuntades en depurar, i la promesa que es va fer a 03-03 sobre per què les funcions pures eren fàcils de provar. Muntaràs Jest sobre Nómada Tasques, aprendràs l'anatomia d'una prova i el patró Preparar-Actuar-Comprovar, dominaràs els matchers que de debò es fan servir, cobriràs la taula de transicions R6 amb proves parametritzades, provaràs codi asíncron, mesuraràs la cobertura sense caure en el seu parany, i escriuràs la teva primera regla amb el cicle vermell-verd-refactor. I tot això sense obrir el navegador ni una sola vegada.

Contingut

  1. Què és una prova automatitzada
  2. Què hi guanya l'equip: tres beneficis i cap no és «detectar bugs»
  3. La piràmide de proves
  4. Jest: instal·lació i primera execució
  5. Configuració per a mòduls ES
  6. Anatomia d'una prova: describe, test i el patró AAA
  7. Els matchers que es fan servir de debò
  8. toBe, toEqual i toStrictEqual: la comparació per referència
  9. Comprovar que alguna cosa llança un error
  10. Ganxos: beforeEach i companyia
  11. L'estat compartit, font de fallades intermitents
  12. Proves parametritzades amb test.each
  13. Provar codi asíncron
  14. Què fa provable un mòdul
  15. Cobertura: què mesura de debò
  16. Anomenar una prova i la regla del motiu únic
  17. TDD: vermell, verd, refactor
  18. Nómada Tasques: la bateria completa del model
  19. Errors Habituals i Consells
  20. Exercicis
  21. Conclusió

  1. Què és una prova automatitzada

Una prova automatitzada és un programa que executa un altre programa i comprova que fa el que ha de fer. Res més. Tota la resta —Jest, els matchers, la cobertura— és infraestructura al voltant d'aquesta idea.

De fet, ja has escrit proves sense saber-ho. Això, a la consola, és una prova manual:

const tauler = new Tauler('Taller Nómada', crearBacklog());
tauler.resum('2026-09-20');
// { total: 6, obertes: 5, horesTotals: 48, horesObertes: 45, vencudes: 1, esforc: 124 }

Mires el resultat, comproves que surt 45 i continues. El problema no és la comprovació: és que tu ets qui l'executa, qui la recorda i qui decideix si el número està bé. Una setmana després, ningú hi torna a mirar. La versió automatitzada diu el mateix, però ho diu sola:

test('el resum del backlog canònic dóna 45 h obertes de 48', () => {
  const tauler = new Tauler('Taller Nómada', crearBacklog());
  expect(tauler.resum('2026-09-20').horesObertes).toBe(45);
});

Aquestes tres línies, executades a cada commit, haurien detectat el cas 3 de 08-01 —l'horesObertes que sumava 48— en el mateix instant en què es va introduir, no setmanes després per un avís de la Lucía.

flowchart LR
    A["Preparar<br/>entrada coneguda"] --> B["Actuar<br/>executar el codi"]
    B --> C["Comprovar<br/>sortida esperada"]
    C -->|coincideixen| D["✓ verd"]
    C -->|difereixen| E["✗ vermell<br/>amb el detall"]

  1. Què hi guanya l'equip: tres beneficis i cap no és «detectar bugs»

Sona contraintuïtiu, però les proves no són especialment bones trobant errors nous. Un error que no se't va acudir no tindrà prova. El que les proves fan extraordinàriament bé és una altra cosa:

1 · Xarxa de seguretat per refactoritzar. Aquest és el benefici gran. A 08-01 algú va "optimitzar" un getter i va trencar el recompte d'hores sense que ningú se n'assabentés. Amb una bateria de proves, aquell canvi es posa vermell en dos segons. La conseqüència és enorme: pots canviar el codi sense por. Sense proves, tota refactorització és una aposta, i la reacció racional és no tocar res; el codi es podreix perquè ningú s'atreveix a millorar-lo.

2 · Documentació executable. Un document que diu «no es pot passar de feta a en curs» pot quedar obsolet i ningú ho nota. Una prova que ho afirma falla el dia en què deixa de ser cert. Llegeix aquest bloc:

describe("Tasca · transicions d'estat (R6)", () => {
  test('pendent → en-curs està permès', () => { … });
  test('en-curs → feta està permès', () => { … });
  test('en-curs → pendent està permès (es pot tornar enrere una tasca)', () => { … });
  test('feta → en-curs llança ErrorDeValidacio: una tasca feta no es reobre', () => { … });
  test('pendent → feta llança ErrorDeValidacio: primer cal començar-la', () => { … });
});

Sense llegir una línia d'implementació ja saps quines són les regles del domini. I aquesta documentació no pot mentir, perquè s'executa.

3 · Pressió cap a un disseny millor. Aquest és el més subtil i el més valuós a llarg termini. Quan una funció és difícil de provar, gairebé sempre és perquè fa massa coses, depèn de massa, o barreja decisió amb efecte. La dificultat de provar és un detector de mal disseny. Ho veuràs amb claredat a l'apartat 14: Tauler.resum() es prova en tres línies perquè rep dades i retorna dades; desarIRenderitzar() no es pot provar sense un navegador perquè barreja tres responsabilitats.

Un quart benefici, més prosaic però decisiu en el dia a dia: el cicle de retroalimentació. Comprovar a mà que les sis regles d'estat continuen funcionant exigeix obrir l'aplicació, crear tasques i prémer botons: cinc minuts si et concentres. La mateixa comprovació automatitzada triga 200 mil·lisegons i s'executa vint vegades per hora sense que hi pensis.

  1. La piràmide de proves

No totes les proves costen ni valen el mateix. La piràmide de proves és el model clàssic per repartir-les:

flowchart TD
    E["Extrem a extrem (E2E)<br/>L'app sencera en un navegador real<br/>~5 %"]
    I["Integració<br/>Diverses peces juntes: model + dades, vista + DOM<br/>~20 %"]
    U["Unitàries<br/>Una unitat aïllada: una classe, una funció<br/>~75 %"]
    E --> I --> U
    style E fill:#fecaca,stroke:#b91c1c
    style I fill:#fde68a,stroke:#b45309
    style U fill:#bbf7d0,stroke:#15803d
Nivell Què prova Velocitat Fragilitat Què et diu una fallada
Unitària Una funció o classe, aïllada Mil·lisegons Molt baixa Exactament quina línia està malament
Integració Diverses peces col·laborant Dècimes de segon Mitjana Que dues peces no s'entenen
E2E L'aplicació sencera, com la fa servir la Marta Segons Alta Que alguna cosa del recorregut no funciona

La forma de piràmide es justifica per dos eixos que van en direccions oposades: en pujar augmenta la confiança (una prova E2E verda significa que l'aplicació funciona de debò) però també el cost i la fragilitat (triga segons, es trenca en canviar un text, i quan falla no diu on).

Dos advertiments sobre aquest model:

  • La piràmide és una guia, no una llei. Els percentatges depenen del projecte. A Proves d'Integració coneixeràs el testing trophy, una variant que dóna més pes a la integració i que encaixa millor amb aplicacions d'interfície.
  • L'antipatró que cal evitar té nom: el con de gelat. Moltes proves E2E lentes a dalt, poques unitàries a sota. La suite triga quaranta minuts, falla de manera intermitent, ningú sap per què, i acaba desactivada.

En aquest mòdul construeixes els tres nivells sobre el mateix projecte: aquí les unitàries del model, a 08-05 la integració, i a 08-06 tres recorreguts E2E ben triats.

  1. Jest: instal·lació i primera execució

Jest és un executor de proves complet: troba els fitxers, els executa en paral·lel, ofereix les assercions, mesura la cobertura i porta dobles de prova incorporats (això últim, a 08-04).

npm install --save-dev jest

Jest descobreix les proves per convenció. Qualsevol d'aquestes ubicacions val:

Patró Exemple Quan convé
*.test.js al costat del codi js/model/tasca.test.js Projectes petits: la prova és al costat
__tests__/ js/model/__tests__/tasca.js Convenció per defecte de Jest
Carpeta proves/ mirall proves/model/tasca.test.js La que farem servir: separa codi de proves

Per a Nómada Tasques fem servir una carpeta mirall, perquè manté neta la carpeta js/ que se serveix al navegador:

nomada-tasques/
  js/                       ← l'aplicació (es desplega)
  proves/                   ← les proves (no es despleguen mai)
    model/
      tasca.test.js
      tauler.test.js
    util/
      dates.test.js
    ajudes/
      backlog-de-prova.js   ← utilitats compartides, sense proves a dins

Els scripts:

{
  "scripts": {
    "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
    "test:watch": "npm test -- --watch",
    "test:cov": "npm test -- --coverage"
  }
}

I l'execució:

$ npm test

 PASS  proves/model/tasca.test.js
 PASS  proves/model/tauler.test.js

Test Suites: 2 passed, 2 total
Tests:       31 passed, 31 total
Snapshots:   0 total
Time:        0.842 s

El mode watch és on Jest canvia la manera de treballar:

npm run test:watch

Es queda executant-se i, a cada desat, torna a llançar només les proves afectades pel que has canviat (fa servir el graf de dependències de les teves importacions per saber-ho). Amb l'editor a l'esquerra i el resultat a la dreta, el cicle escriure → desar → veure verd baixa d'un segon. Les seves dreceres interactives:

Tecla Què fa
a Executar totes les proves
f Només les que van fallar l'última vegada
p Filtrar per patró de nom de fitxer
t Filtrar per nom de prova
o Només el relacionat amb fitxers modificats a Git

  1. Configuració per a mòduls ES

Aquí hi ha un escull real que convé entendre, perquè és la primera pedra amb què ensopega tothom.

Jest va néixer quan Node feia servir CommonJS (require). El teu projecte fa servir mòduls ES (import/export), que és el que vas estudiar a 05-04 i el que el navegador executa de manera nativa. Hi ha dos camins per conciliar-los:

Camí A · Mòduls ES natius (el que fem servir). Node executa els import de debò; no hi ha cap transformació pel mig, així que el que es prova és exactament el que es desplega.

// package.json
{
  "type": "module",
  "scripts": {
    "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
  }
}
// jest.config.js
export default {
  // 'node' n'hi ha prou per al model: no toca DOM. A 08-05 canviarà a 'jsdom'.
  testEnvironment: 'node',

  // Sense transformacions: Node entén els import tal qual.
  transform: {},

  testMatch: ['**/proves/**/*.test.js'],

  // Fitxers de l'aplicació que compten per a la cobertura
  collectCoverageFrom: ['js/**/*.js', '!js/app.js'],

  // Sortida més llegible quan alguna cosa falla
  verbose: false
};

La bandera --experimental-vm-modules de Node és el que habilita els mòduls ES dins de l'entorn aïllat de Jest. El nom espanta més del que hauria: és el camí oficialment documentat i funciona de manera estable. Com que no es pot passar per jest.config.js, va al script (o a NODE_OPTIONS).

Camí B · Transformar amb Babel. S'instal·la babel-jest amb un preajust que converteix els import a require abans d'executar. Avantatge: tot funciona sense banderes i amb suport ple de les funcions de simulació de mòduls. Inconvenient: proves un codi transformat, no el que despleguen, i hi ha una capa més per configurar i que pot fallar.

// babel.config.js — només si tries el camí B
export default {
  presets: [['@babel/preset-env', { targets: { node: 'current' } }]]
};
Camí A · ESM natiu Camí B · Babel
Fidelitat amb producció Alta: mateix codi Mitjana: transformat
Configuració Una bandera de Node Babel + preajust
jest.mock de mòduls Requereix l'API d'ESM (08-04) Directe
Recomanació aquí Aquest Alternativa vàlida

Triem el camí A perquè encaixa amb l'esperit del projecte: Nómada Tasques se serveix sense empaquetador, i provar el codi real evita la classe sencera d'errors «funcionava a les proves i no al navegador».

  1. Anatomia d'una prova: describe, test i el patró AAA

// proves/model/tasca.test.js
import { Tasca } from '../../js/model/tasca.js';
import { ErrorDeValidacio } from '../../js/model/errors.js';

const DADES_VALIDES = {
  id: 1,
  titol: 'Redissenyar la sala polivalent',
  responsable: 'Iván',
  prioritat: 'alta',
  estat: 'en-curs',
  etiquetes: ['espai', 'disseny'],
  horesEstimades: 12,
  dataLimit: '2026-09-30',
  revisor: 'Marta'
};

describe('Tasca', () => {
  describe('construcció', () => {
    test('conserva el títol sense espais sobrants', () => {
      // Preparar (Arrange)
      const dades = { ...DADES_VALIDES, titol: '  Redissenyar la sala  ' };

      // Actuar (Act)
      const tasca = new Tasca(dades);

      // Comprovar (Assert)
      expect(tasca.titol).toBe('Redissenyar la sala');
    });
  });
});

Les tres peces:

  • describe(nom, fn) agrupa proves relacionades. Es pot imbricar, i imbricar-lo bé produeix una sortida que es llegeix com un índex del comportament. No és obligatori, però tan bon punt passes de deu proves per fitxer, és el que les fa navegables.
  • test(nom, fn) és una prova. it és exactament el mateix, un àlies que ve de la tradició d'escriure it('should trim the title'). Fes servir el que prefereixis, però fes servir sempre el mateix a tot el projecte.
  • expect(valor).matcher(esperat) és l'asserció. Si es compleix, no passa res; si no, Jest llança un error amb la diferència detallada.

El patró AAA (Arrange-Act-Assert, o Preparar-Actuar-Comprovar) és l'estructura que ha de tenir el cos de tota prova:

Fase Què fas Senyal d'alarma
Preparar Construeixes les dades i l'objecte sota prova Si ocupa 30 línies, l'objecte és massa complicat
Actuar Una sola crida: el que estàs provant Si hi ha tres crides, probablement són tres proves
Comprovar Les assercions sobre el resultat Si comproves cinc coses sense relació, divideix

Separar les tres fases amb línies en blanc (o amb comentaris al principi, mentre t'hi acostumes) és un d'aquells costums petits que fan que les proves alienes es llegeixin en cinc segons.

  1. Els matchers que es fan servir de debò

Jest porta desenes de matchers. Aquests són els que cobreixen el 95 % dels casos:

Matcher Comprova Exemple
toBe(v) Igualtat per identitat (Object.is) expect(t.horesEstimades).toBe(12)
toEqual(v) Igualtat estructural, recursiva expect(t.etiquetes).toEqual(['espai', 'disseny'])
toStrictEqual(v) Estructural + tipus de classe + claus undefined expect(t.toJSON()).toStrictEqual({ … })
toContain(v) Un array conté un valor (per identitat) expect(t.etiquetes).toContain('disseny')
toContainEqual(v) Un array conté un objecte equivalent expect(tasques).toContainEqual({ id: 3, … })
toHaveLength(n) .length d'array o cadena expect(tauler.obertes).toHaveLength(5)
toMatchObject(v) L'objecte conté almenys aquestes propietats expect(r).toMatchObject({ horesObertes: 45 })
toThrow(v) La funció llança (opcionalment, alguna cosa concreta) expect(() => new Tasca({})).toThrow(ErrorDeValidacio)
toBeCloseTo(n, d) Números decimals, amb tolerància expect(mitjana).toBeCloseTo(8.166, 2)
toBeNull() Exactament null expect(tauler.cercarPerId(99)).toBeNull()
toBeUndefined() Exactament undefined expect(t.revisor).toBeUndefined()
toBeTruthy() / toBeFalsy() Veracitat (01-07) expect(t.oberta).toBeTruthy()
toBeGreaterThan(n) Comparació numèrica expect(r.esforc).toBeGreaterThan(100)
not Nega qualsevol matcher expect(t.etiquetes).not.toContain('web')

Dos consells sobre la seva elecció:

  • Prefereix el matcher més específic. expect(tauler.obertes).toHaveLength(5) falla dient «esperava longitud 5, he rebut 6»; expect(tauler.obertes.length === 5).toBe(true) falla dient «esperava true, he rebut false», que no ajuda gens. El missatge de fallada és part del valor de la prova.
  • Desconfia de toBeTruthy. expect(t.oberta).toBeTruthy() passa també si oberta retorna 'si', 1 o []. Si esperes un booleà, escriu toBe(true).

toMatchObject mereix el seu propi comentari perquè és el que millor equilibra precisió i manteniment en objectes grans:

// Comprova NOMÉS el que importa a aquesta prova; la resta pot canviar sense trencar-la
expect(tauler.resum(AVUI)).toMatchObject({ horesObertes: 45, vencudes: 1 });

// Davant d'això, que es trenca si demà el resum afegeix un camp nou:
expect(tauler.resum(AVUI)).toStrictEqual({
  total: 6, obertes: 5, horesTotals: 48, horesObertes: 45, vencudes: 1, esforc: 124
});

Totes dues versions tenen el seu lloc. La segona és apropiada precisament per al resum canònic, on vols que afegir un camp obligui a revisar la prova. La primera, per a tota la resta.

  1. toBe, toEqual i toStrictEqual: la comparació per referència

Aquest és el punt on més s'ensopega, i és exactament el tema de 01-07 i 04-08 aplicat a les proves.

const a = { id: 1, titol: 'Redissenyar la sala' };
const b = { id: 1, titol: 'Redissenyar la sala' };

expect(a).toBe(b);       // ✗ FALLA: són dos objectes diferents en memòria
expect(a).toEqual(b);    // ✓ passa: tenen el mateix contingut
expect(a).toBe(a);       // ✓ passa: és literalment el mateix objecte

toBe fa servir Object.is, que per a objectes és comparació per referència: dos objectes amb el mateix contingut són diferents. És el parany de 04-08, ara amb conseqüències a les proves.

La regla operativa:

Què compares Matcher
Números, cadenes, booleans, null, undefined toBe
Objectes, arrays, mapes, conjunts toEqual o toStrictEqual
Que sigui literalment el mateix objecte (identitat) toBe — a propòsit

I la diferència entre toEqual i toStrictEqual, que són tres diferències concretes:

// 1 · Propietats amb valor undefined
expect({ id: 1, revisor: undefined }).toEqual({ id: 1 });        // ✓ passa: toEqual les ignora
expect({ id: 1, revisor: undefined }).toStrictEqual({ id: 1 });  // ✗ falla: són diferents

// 2 · Tipus de classe
class Tasca { constructor() { this.id = 1; } }
expect(new Tasca()).toEqual({ id: 1 });          // ✓ passa: mateix contingut
expect(new Tasca()).toStrictEqual({ id: 1 });    // ✗ falla: una és Tasca, l'altre objecte pla

// 3 · Forats en arrays
expect([, 1]).toEqual([undefined, 1]);           // ✓ passa
expect([, 1]).toStrictEqual([undefined, 1]);     // ✗ falla

La primera diferència és la que importa a Nómada Tasques. R8 diu que una tasca sense responsable té responsable: null, mai '' ni undefined. Amb toEqual, un toJSON() que retornés { …, revisor: undefined } passaria la prova i després produiria un JSON sense el camp revisor en serialitzar —perquè JSON.stringify elimina els undefined (04-08)— i desDeJSON reconstruiria una cosa diferent. Amb toStrictEqual, la prova falla i l'error es veu abans d'arribar a l'emmagatzematge.

Regla per a aquest projecte: fes servir toStrictEqual per comprovar la sortida de toJSON(), i toEqual per a la resta.

I un ús de toBe sobre objectes que que és deliberat, comprovant les còpies defensives de 05-03:

test("el getter tasques retorna una còpia, no l'array intern", () => {
  const tauler = new Tauler('Taller Nómada', crearBacklog());

  expect(tauler.tasques).not.toBe(tauler.tasques);   // dues crides, dos arrays diferents
  expect(tauler.tasques).toEqual(tauler.tasques);    // però amb el mateix contingut
});

Aquesta prova protegeix una decisió de disseny concreta: si algú "optimitzés" el getter retornant this.#tasques directament, un .sort() de la vista reordenaria el model. La prova es posaria vermella a l'instant.

  1. Comprovar que alguna cosa llança un error

Les regles de negoci de Nómada Tasques s'expressen llançant ErrorDeValidacio. Provar que es llança és tan important com provar que alguna cosa funciona.

El matcher és toThrow, i hi ha un detall que fa fallar tothom la primera vegada:

// ❌ MALAMENT: l'excepció es llança en avaluar l'argument, abans d'arribar a expect
expect(new Tasca({ titol: '' })).toThrow(ErrorDeValidacio);

// ✅ BÉ: es passa una FUNCIÓ que Jest executarà dins d'un try/catch
expect(() => new Tasca({ titol: '' })).toThrow(ErrorDeValidacio);

toThrow admet quatre formes d'argument, de menys a més específica:

const crear = () => new Tasca({ id: 1, titol: '   ', horesEstimades: 4 });

expect(crear).toThrow();                              // 1 · llança alguna cosa
expect(crear).toThrow(ErrorDeValidacio);              // 2 · llança AQUEST tipus
expect(crear).toThrow('El titol no pot estar buit.'); // 3 · el missatge CONTÉ aquest text
expect(crear).toThrow(/titol/i);                      // 4 · el missatge casa amb l'expressió regular

Recomanació: fes servir la forma 2, el tipus. El tipus és estable; el text del missatge canvia quan algú millora la redacció, i no vols que millorar un missatge posi en vermell deu proves.

Però hi ha una cosa que toThrow no comprova i que en aquest projecte és essencial: el camp camp de l'error. Aquest camp és el que a 06-07 permetia al formulari moure el focus a l'<input> correcte. Per comprovar-ho hi ha dos patrons:

// Patró A · try/catch explícit amb expect.assertions
test("l'error d'hores identifica el camp horesEstimades", () => {
  expect.assertions(3);            // ← garanteix que les 3 assercions s'executen

  try {
    new Tasca({ id: 1, titol: 'Prova', horesEstimades: 99 });
  } catch (error) {
    expect(error).toBeInstanceOf(ErrorDeValidacio);
    expect(error.camp).toBe('horesEstimades');
    expect(error.valorRebut).toBe(99);
  }
});

// Patró B · capturar amb una funció auxiliar (més llegible quan es repeteix)
function capturar(fn) {
  try { fn(); return null; } catch (error) { return error; }
}

test("l'error d'hores identifica el camp horesEstimades", () => {
  const error = capturar(() => new Tasca({ id: 1, titol: 'Prova', horesEstimades: 99 }));

  expect(error).toBeInstanceOf(ErrorDeValidacio);
  expect(error).toMatchObject({ camp: 'horesEstimades', valorRebut: 99 });
});

expect.assertions(n) del patró A és imprescindible i mereix explicació. Sense aquesta línia, si el codi deixés de llançar, el try acabaria sense error, el catch no s'executaria, la prova no comprovaria res… i passaria en verd. Una prova que passa quan el comportament s'ha trencat és pitjor que no tenir prova. expect.assertions(3) obliga que s'executin exactament tres assercions; si no, Jest falla.

  1. Ganxos: beforeEach i companyia

Les proves d'un mateix describe solen necessitar la mateixa preparació. Els ganxos (hooks) la centralitzen:

Ganxo Quan s'executa Ús típic
beforeEach(fn) Abans de cada prova Crear un tauler net. El més fet servir amb diferència
afterEach(fn) Després de cada prova Netejar: restaurar dobles, buidar emmagatzematge
beforeAll(fn) Una vegada, abans de tot el bloc Preparació cara i de només lectura
afterAll(fn) Una vegada, al final Tancar recursos
describe('Tauler', () => {
  let tauler;

  beforeEach(() => {
    // Un tauler NOU per a cada prova: ningú hereta els canvis del veí
    tauler = new Tauler('Taller Nómada', crearBacklog());
  });

  test('afegeix les sis tasques del backlog', () => {
    expect(tauler.total).toBe(6);
  });

  test("canviar d'estat no altera el total", () => {
    tauler.canviarEstat(2, 'en-curs');
    expect(tauler.total).toBe(6);
  });
});

Aquí es cobra el disseny de 05-04: crearBacklog() és una funció que retorna instàncies noves, no un array exportat ja construït. Si fos un array compartit, el canviarEstat(2, …) de la segona prova contaminaria la següent. Aquella decisió, presa tres mòduls abans per motius d'encapsulació, és avui el que fa que les proves siguin independents. És un bon exemple de com el bon disseny paga interessos en llocs inesperats.

L'ordre d'execució amb describe imbricats segueix una regla clara: tots els beforeEach de fora cap a dins, la prova, i tots els afterEach de dins cap a fora.

describe('Tauler', () => {
  beforeEach(() => console.log('1 · fora'));

  describe('amb una tasca tancada', () => {
    beforeEach(() => console.log('2 · dins'));

    test('compta 4 obertes', () => console.log('3 · prova'));
  });
});
// Sortida: 1 · fora → 2 · dins → 3 · prova

  1. L'estat compartit, font de fallades intermitents

Aquest apartat és curt i crític. La regla: cada prova ha de poder executar-se sola, en qualsevol ordre, i donar el mateix resultat.

Quan això s'incompleix, apareix la fallada més desesperant de totes: la prova que passa sola i falla a la suite, o al revés. Mira l'error:

// ❌ MALAMENT: el tauler es crea UNA vegada i totes les proves el comparteixen
const tauler = new Tauler('Taller Nómada', crearBacklog());

test('la tasca 2 es pot començar', () => {
  tauler.canviarEstat(2, 'en-curs');
  expect(tauler.cercarPerId(2).estat).toBe('en-curs');
});

test('el backlog té 5 tasques obertes', () => {
  expect(tauler.obertes).toHaveLength(5);      // passa… fins que algú en tanqui una a dalt
});

test('la tasca 2 comença en pendent', () => {
  expect(tauler.cercarPerId(2).estat).toBe('pendent');   // ✗ FALLA per culpa de la primera
});

La tercera prova és correcta, i falla. I falla només si la primera s'ha executat abans, cosa que fa que executar-la aïllada doni verd i sigui impossible d'entendre.

// ✅ BÉ: beforeEach reconstrueix l'estat
let tauler;
beforeEach(() => { tauler = new Tauler('Taller Nómada', crearBacklog()); });

Quatre focus d'estat compartit que cal vigilar:

Focus Com es manifesta Solució
Objecte creat a l'àmbit del mòdul Com l'exemple de dalt beforeEach
Un array o mapa const mutat per les proves S'omple a poc a poc Recrear a beforeEach
localStorage / sessionStorage Dades de la prova anterior Netejar a afterEach (08-04)
Mòduls amb estat intern Un comptador d'ids que no es reinicia Reiniciar explícitament, o injectar-lo

Aquest últim cas té un exemple directe al projecte: crearGeneradorIds de 03-04 manté un comptador al seu closure. Si el mòdul l'instancia en importar-se, la primera prova obtindrà l'id 7 i la segona el 8; si algú reordena les proves, totes dues fallen. La solució és la mateixa decisió de sempre: exportar la fàbrica, no la instància, i que cada prova creï la seva.

Jest, a més, executa cada fitxer de proves en un entorn aïllat amb el seu propi registre de mòduls. Això protegeix entre fitxers, però no dins d'un mateix fitxer. La disciplina del beforeEach continua sent teva.

  1. Proves parametritzades amb test.each

La taula de transicions R6 té nou combinacions possibles (tres estats d'origen × tres de destí). Escriure nou proves gairebé idèntiques és tediós i, sobretot, fa que afegir un estat nou signifiqui escriure tres proves més a mà.

test.each rep una taula i genera una prova per fila:

describe("Tasca · transicions d'estat (R6)", () => {
  // ── Les permeses ────────────────────────────────────────────────────
  test.each([
    ['pendent',  'en-curs'],
    ['en-curs',  'feta'],
    ['en-curs',  'pendent']
  ])('permet passar de %s a %s', (origen, desti) => {
    const tasca = new Tasca({ ...DADES_VALIDES, estat: origen });

    tasca.canviarEstat(desti);

    expect(tasca.estat).toBe(desti);
  });

  // ── Les prohibides ──────────────────────────────────────────────────
  test.each([
    ['pendent',  'pendent'],
    ['pendent',  'feta'],       // primer cal començar-la abans d'acabar-la
    ['en-curs',  'en-curs'],
    ['feta',     'pendent'],    // una tasca feta no es reobre
    ['feta',     'en-curs'],
    ['feta',     'feta']
  ])('prohibeix passar de %s a %s', (origen, desti) => {
    const tasca = new Tasca({ ...DADES_VALIDES, estat: origen });

    expect(() => tasca.canviarEstat(desti)).toThrow(ErrorDeValidacio);
  });
});

Sortida:

  Tasca · transicions d'estat (R6)
    ✓ permet passar de pendent a en-curs
    ✓ permet passar de en-curs a feta
    ✓ permet passar de en-curs a pendent
    ✓ prohibeix passar de pendent a pendent
    ✓ prohibeix passar de pendent a feta
    ✓ prohibeix passar de en-curs a en-curs
    ✓ prohibeix passar de feta a pendent
    ✓ prohibeix passar de feta a en-curs
    ✓ prohibeix passar de feta a feta

Nou proves independents, cadascuna amb el seu nom llegible, i la taula completa coberta sense forats. Fixa't que les dues taules juntes sumen exactament les nou combinacions: això és una comprovació d'exhaustivitat que es veu d'una ullada.

Els marcadors del nom són %s (cadena), %d (número), %o (objecte) i %p (amb format). I existeix una segona sintaxi, amb plantilla etiquetada, que es llegeix encara millor quan hi ha més de dues columnes:

describe("Tasca · validació d'hores (R3)", () => {
  test.each`
    hores   | valid    | motiu
    ${0}    | ${false} | ${'zero no és un valor vàlid'}
    ${-3}   | ${false} | ${'negatiu'}
    ${0.5}  | ${true}  | ${'mitja hora és admissible'}
    ${1}    | ${true}  | ${'límit inferior'}
    ${40}   | ${true}  | ${'límit superior exacte'}
    ${41}   | ${false} | ${'supera el màxim setmanal'}
    ${NaN}  | ${false} | ${'NaN no és comparable'}
    ${'12'} | ${false} | ${'una cadena no és un número'}
  `('horesEstimades $hores → $valid ($motiu)', ({ hores, valid }) => {
    const crear = () => new Tasca({ ...DADES_VALIDES, horesEstimades: hores });

    if (valid) expect(crear().horesEstimades).toBe(hores);
    else expect(crear).toThrow(ErrorDeValidacio);
  });
});

Aquesta taula és especialment valuosa perquè recull els valors frontera: 0, 1, 40, 41. La majoria de les fallades de validació viuen exactament en aquestes vores (un > on havia d'anar un >=), i una taula els fa explícits. Les dues últimes files —NaN i la cadena '12'— documenten a més dues decisions de disseny que no són òbvies: el setter exigeix typeof valor === 'number', així que '12' es rebutja encara que sembli un número, i NaN falla perquè cap comparació amb NaN és certa (01-07).

  1. Provar codi asíncron

Tot el del Mòdul 5 aplicat a les proves. La regla fonamental: si la prova és asíncrona, Jest ho ha de saber, o acabarà abans que es comprovi res.

// ❌ MALAMENT: la prova acaba abans que la promesa es resolgui. Passa sempre.
test('importa les tasques', () => {
  carregarTauler().then((tauler) => {
    expect(tauler.total).toBe(6);       // ← això s'executa quan ningú mira
  });
});

// ✅ BÉ: la funció és async i s'espera
test('importa les tasques', async () => {
  const tauler = await carregarTauler();
  expect(tauler.total).toBe(6);
});

// ✅ TAMBÉ BÉ: retornar la promesa
test('importa les tasques', () => {
  return carregarTauler().then((tauler) => {
    expect(tauler.total).toBe(6);
  });
});

La primera versió és un fals verd, el pitjor resultat possible: la prova és a la suite, es veu en verd i no comprova res. La regla jest/valid-expect que vas configurar a 08-02 detecta algunes variants d'aquesta fallada; la disciplina d'escriure sempre async/await l'evita del tot.

Per a promeses, Jest ofereix a més dos modificadors que estalvien cerimònia:

// resolves: aplica el matcher al valor RESOLT
await expect(carregarTauler()).resolves.toMatchObject({ total: 6 });

// rejects: aplica el matcher al motiu del REBUIG
await expect(carregarTauler('trencat')).rejects.toThrow(ErrorDeDades);
await expect(carregarTauler('trencat')).rejects.toBeInstanceOf(ErrorDeDades);

No oblidis l'await davant d'expect en fer servir resolves o rejects: sense ell, l'asserció retorna una promesa que ningú espera i tornes al fals verd.

I el patró per comprovar propietats de l'error rebutjat, que en aquest projecte cal sovint:

test('importar dades corrompudes llança ErrorDeDades amb la causa a dins', async () => {
  expect.assertions(2);

  try {
    await repositori.carregarDesDe('{{{ això no és JSON');
  } catch (error) {
    expect(error).toBeInstanceOf(ErrorDeDades);
    expect(error.causa).toBeInstanceOf(SyntaxError);
  }
});

Un avís sobre els temporitzadors: si el teu codi fa servir setTimeout —com el dormir() d'ambReintents a 07-03—, una prova que l'executi de debò trigarà segons i farà lenta la suite. La solució són els temporitzadors falsos, i és tema de la lliçó següent, Dobles de Prova. Aquí ens quedem a les proves del model, que són pures i no esperen res.

  1. Què fa provable un mòdul

Aquí es tanca la promesa de 03-03. Compara aquestes dues funcions:

// ── A · Funció pura: entra una dada, surt una dada ────────────────────
export function estaVencuda(dataLimit, estat, avui = AVUI) {
  return dataLimit < avui && estat !== 'feta';
}

// ── B · Funció impura: depèn del món i el modifica ────────────────────
export function marcarVencudes() {
  const avui = new Date().toISOString().slice(0, 10);        // ① el rellotge real
  const desat = JSON.parse(localStorage.getItem('nomada:tauler:v1'));   // ② el magatzem
  for (const t of desat.tasques) {
    if (t.dataLimit < avui && t.estat !== 'feta') {
      document.querySelector(`[data-id="${t.id}"]`).classList.add('vencuda');  // ③ el DOM
    }
  }
}

Provar A:

test('una tasca amb data passada i sense acabar està vençuda (R10)', () => {
  expect(estaVencuda('2026-09-05', 'pendent', '2026-09-20')).toBe(true);
});

Una línia. Sense preparació, sense neteja, sense navegador, en menys d'un mil·lisegon, i amb el mateix resultat avui, demà i d'aquí a tres anys.

Provar B exigeix un DOM, un localStorage, i congelar el rellotge del sistema, perquè el dia en què s'executi canvia el resultat. És possible —ho faràs a 08-04 i 08-05— però costa vint vegades més i la prova resultant és fràgil.

Les tres propietats d'una funció pura (03-03) expliquen la diferència:

Propietat Conseqüència per a la prova
El resultat depèn només dels arguments No cal preparar cap entorn
No modifica res fora d'ella mateixa No cal netejar després
Amb la mateixa entrada, sempre la mateixa sortida Mai és intermitent

La segona eina és la injecció de dependències, i a Nómada Tasques ja està aplicada en diversos llocs sense que en diguéssim així:

// js/util/dates.js — la data ENTRA com a paràmetre, amb un valor per defecte
export function estaVencuda(dataLimit, estat, avui = AVUI) { … }

// js/model/tasca.js
estaVencuda(avui = AVUI) { return dataVencuda(this.dataLimit, this.#estat, avui); }

// js/dades/repositori-local.js — el magatzem ENTRA pel constructor
constructor({ clau = CLAU, magatzem } = {}) {
  this.#magatzem = magatzem ?? (this.#persistent ? window.localStorage : magatzemEnMemoria());
}

Aquest paràmetre avui amb valor per defecte és el que permet escriure:

test('el pressupost de la fusteria està vençut el 20 de setembre', () => {
  const tauler = new Tauler('Taller Nómada', crearBacklog());

  expect(tauler.vencudes('2026-09-20')).toHaveLength(1);
  expect(tauler.vencudes('2026-09-20')[0].titol).toBe('Pressupost de la fusteria');
});

La prova donarà el mateix resultat el dia que s'executi, perquè el temps entra com a dada. Sense aquest paràmetre, la prova passaria avui i fallaria l'1 d'octubre, quan la tasca 3 també estigués vençuda. Aquest tipus de fallada —una suite que es trenca sola un dimarts al matí sense que ningú hagi tocat res— és de les que més confiança destrueixen.

I el mateix constructor de RepositoriLocal que accepta un magatzem és el que permetrà provar-lo sense navegador a 08-05. La lliçó de fons: una dependència que entra per paràmetre és una dependència substituïble a la prova; una que es pren de l'àmbit global, no.

flowchart TD
    A["La funció depèn<br/>només dels seus arguments?"] -->|Sí| B["Funció pura<br/>prova trivial"]
    A -->|No| C["La dependència<br/>entra per paràmetre?"]
    C -->|Sí| D["Injectable<br/>prova fàcil (08-04)"]
    C -->|No| E["Acoblada a l'entorn<br/>cal redissenyar<br/>o pujar a integració (08-05)"]
    style B fill:#bbf7d0,stroke:#15803d
    style D fill:#fde68a,stroke:#b45309
    style E fill:#fecaca,stroke:#b91c1c

  1. Cobertura: què mesura de debò

La cobertura mesura quin percentatge del teu codi s'executa en passar les proves.

npm run test:cov
-------------------------|---------|----------|---------|---------|-------------------
File                     | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------------------|---------|----------|---------|---------|-------------------
All files                |   84.21 |    78.94 |   88.23 |   84.05 |
 js/model                |   97.14 |    95.45 |     100 |   97.10 |
  errors.js              |     100 |      100 |     100 |     100 |
  tasca.js               |   96.42 |    94.11 |     100 |   96.36 | 88
  tauler.js              |     100 |      100 |     100 |     100 |
 js/util                 |     100 |      100 |     100 |     100 |
  dates.js               |     100 |      100 |     100 |     100 |
  format.js              |     100 |      100 |     100 |     100 |
 js/dades                |   42.85 |    28.57 |   50.00 |   42.30 | 34-58,71-88
  backlog.js             |     100 |      100 |     100 |     100 |
  repositori-local.js    |   31.25 |    20.00 |   33.33 |   30.76 | 34-58,71-88
-------------------------|---------|----------|---------|---------|-------------------

Les quatre columnes mesuren coses diferents:

Mètrica Què compta Per què importa
Statements Sentències executades La mesura més gruixuda
Branch Branques de cada condició (if/else, &&, ?:, ??) La més informativa: revela camins no provats
Functions Funcions invocades almenys una vegada Detecta codi mort o sense provar
Lines Línies executades Gairebé igual que statements

Branch és la que cal mirar. Una condició com dataLimit < avui && estat !== 'feta' té quatre combinacions; recórrer-ne una sola dóna 100 % de línies i 25 % de branques. L'informe HTML (coverage/lcov-report/index.html) les pinta en groc, i navegar-hi és la millor manera de trobar forats reals.

I ara la part important: què NO mesura la cobertura.

// Aquesta "prova" dóna 100 % de cobertura de la funció… i no comprova res
test('resum', () => {
  const tauler = new Tauler('Taller Nómada', crearBacklog());
  tauler.resum('2026-09-20');     // s'executa sencera. Zero assercions.
});

La cobertura mesura execució, no verificació. Un 100 % amb proves sense assercions és un 100 % buit. Per això vas configurar jest/expect-expect a la lliçó anterior.

Tres afirmacions que convé tenir clares:

  • Cobertura baixa és un senyal fiable de problema. Si repositori-local.js està al 31 %, hi ha camins importants sense provar (aquí, precisament, la gestió de dades corrompudes i la reserva en memòria).
  • Cobertura alta no és senyal de res. No diu que les assercions siguin correctes, ni que hagis provat els casos límit, ni que la lògica sigui la que el negoci vol.
  • Perseguir el 100 % és contraproduent. Els últims punts percentuals solen ser en branques defensives que gairebé mai passen, i es "cobreixen" amb proves artificials que no aporten i que cal mantenir. El cost creix exponencialment i el valor decreix.

Una política raonable, a jest.config.js:

export default {
  // …
  coverageThreshold: {
    global:                  { branches: 70, functions: 80, lines: 80 },
    './js/model/':           { branches: 95, functions: 100, lines: 95 },   // el cor: exigent
    './js/vista/':           { branches: 50, functions: 60, lines: 60 }     // el DOM: integració (08-05)
  }
};

Llindars per carpeta, no un de global: el model, que és pur i conté les regles de negoci, mereix exigència màxima; la vista es cobreix millor amb proves d'integració. I els llindars fan fallar la construcció si la cobertura baixa, que és la seva veritable utilitat: eviten que s'afegeixi codi sense provar a poc a poc.

  1. Anomenar una prova i la regla del motiu únic

El nom d'una prova es llegeix dues vegades: quan algú busca què està cobert, i —sobretot— quan falla a la integració contínua. En aquest segon moment, un bon nom estalvia obrir el fitxer.

// ❌ No diuen res
test('tauler', …);
test('funciona', …);
test('test 3', …);
test('canviarEstat', …);

// ✅ Subjecte + comportament + condició
test('canviarEstat llança ErrorDeValidacio si la tasca no existeix', …);
test('el resum del backlog canònic dóna 45 h obertes de 48', …);
test('afegir rebutja un id duplicat (R1)', …);
test('les etiquetes es normalitzen a minúscules i sense duplicats (R9)', …);

La plantilla que funciona: <què> <fa o no fa> <quan>. I una recomanació d'aquest projecte: cita la regla de negoci entre parèntesis. Quan una prova de R6 es posa vermella, saber que és R6 porta directe a la conversa correcta amb la Marta.

La regla del motiu únic. Una prova ha de fallar per un sol motiu. Compara:

// ❌ Si falla, què està trencat? Cal llegir l'informe amb lupa
test('el tauler funciona', () => {
  const tauler = new Tauler('Taller Nómada', crearBacklog());
  expect(tauler.total).toBe(6);
  tauler.canviarEstat(2, 'en-curs');
  expect(tauler.cercarPerId(2).estat).toBe('en-curs');
  expect(tauler.resum(AVUI).horesObertes).toBe(45);
  expect(() => tauler.afegir(new Tasca(DADES_VALIDES))).toThrow();
});

// ✅ Quatre proves, quatre noms, quatre diagnòstics
test('el backlog canònic té 6 tasques', …);
test('canviarEstat aplica la transició a la tasca indicada', …);
test('el resum del backlog canònic dóna 45 h obertes', …);
test('afegir rebutja un id duplicat (R1)', …);

El matís: «un sol motiu de fallada» no significa «una sola asserció». Comprovar tres propietats del mateix resultat és un sol motiu:

test("l'error d'hores identifica el camp i el valor rebut", () => {
  const error = capturar(() => new Tasca({ ...DADES_VALIDES, horesEstimades: 99 }));

  expect(error).toBeInstanceOf(ErrorDeValidacio);   // tres assercions…
  expect(error.camp).toBe('horesEstimades');        // …sobre el MATEIX error…
  expect(error.valorRebut).toBe(99);                // …= un sol motiu de fallada
});

  1. TDD: vermell, verd, refactor

El desenvolupament dirigit per proves (TDD) inverteix l'ordre habitual: primer la prova, després el codi.

flowchart LR
    R["🔴 VERMELL<br/>Escriu una prova<br/>que falla"] --> V["🟢 VERD<br/>El codi MÍNIM<br/>que la fa passar"]
    V --> F["🔵 REFACTOR<br/>Millora el codi<br/>amb la prova de xarxa"]
    F --> R

L'apliquem a una regla nova que demana la Marta: R11 — una tasca vençuda no pot tenir prioritat 'baixa'; si venç i continua oberta, s'escala automàticament a 'alta'.

Volta 1 · Vermell. La prova primer, abans de tocar tasca.js:

describe('Tasca · escalat de prioritat (R11)', () => {
  test('una tasca vençuda i de prioritat baixa escala a alta', () => {
    const tasca = new Tasca({ ...DADES_VALIDES, prioritat: 'baixa',
                              dataLimit: '2026-09-05', estat: 'pendent' });

    expect(tasca.prioritatEfectiva('2026-09-20')).toBe('alta');
  });
});
● Tasca · escalat de prioritat (R11) › una tasca vençuda i de prioritat baixa escala a alta
  TypeError: tasca.prioritatEfectiva is not a function

Vermell, i vermell pel motiu correcte: el mètode no existeix. Aquest pas no és una formalitat: verificar que la prova falla demostra que la prova de debò comprova alguna cosa. Una prova que passa abans d'escriure el codi està mal escrita.

Volta 1 · Verd. El codi mínim:

// js/model/tasca.js
prioritatEfectiva(avui = AVUI) {
  return 'alta';       // sí, així de mínim. Ja ho corregiran les proves següents.
}

Verd. Sembla una trampa, i és intencionat: obliga a escriure la prova següent, la que distingeix els casos.

Volta 2 · Vermell. El cas contrari:

test('una tasca no vençuda conserva la seva prioritat', () => {
  const tasca = new Tasca({ ...DADES_VALIDES, prioritat: 'baixa', dataLimit: '2026-11-30' });

  expect(tasca.prioritatEfectiva('2026-09-20')).toBe('baixa');
});
Expected: "baixa"   Received: "alta"

Volta 2 · Verd.

prioritatEfectiva(avui = AVUI) {
  return this.estaVencuda(avui) ? 'alta' : this.prioritat;
}

Volta 3 · Vermell. El cas límit que revela un matís de l'enunciat:

test('una tasca vençuda de prioritat mitjana conserva la seva prioritat', () => {
  const tasca = new Tasca({ ...DADES_VALIDES, prioritat: 'mitjana', dataLimit: '2026-09-05' });

  expect(tasca.prioritatEfectiva('2026-09-20')).toBe('mitjana');
});

Falla: la implementació escala tot el que està vençut, i la regla deia «prioritat baixa».

Volta 3 · Verd.

prioritatEfectiva(avui = AVUI) {
  return this.estaVencuda(avui) && this.prioritat === 'baixa' ? 'alta' : this.prioritat;
}

Refactor. Amb les tres proves en verd, es pot millorar sense por. L'expressió ternària comença a demanar un nom:

// js/model/tasca.js
/** R11: el que està vençut i de baixa prioritat escala; la resta conserva la seva. */
get #vencudaIDeBaixaPrioritat() {
  return this.estaVencuda() && this.prioritat === 'baixa';
}

prioritatEfectiva(avui = AVUI) {
  return this.estaVencuda(avui) && this.prioritat === 'baixa' ? 'alta' : this.prioritat;
}

Les tres proves continuen verdes després del canvi: això és exactament el que significa «xarxa de seguretat».

Què aporta TDD, honestament:

A favor En contra
Garanteix que tot el codi té prova Costa més al principi, fins que li agafes el ritme
Força a dissenyar la interfície abans que la implementació No encaixa bé quan explores i no saps què vols
Impedeix escriure codi de més («per si de cas») Requereix disciplina sostinguda
Documenta cada decisió mentre es pren Amb requisits que canvien cada hora, es refà molt

No cal fer TDD sempre. Però hi ha un moment en què és indiscutiblement la millor opció: en corregir un error. Escriu primer la prova que el reprodueix —el pas 6 del mètode de 08-01—, comprova-la vermella, i arregla'l. Així garanteixes que l'arranjament funciona i que l'error no torna.

  1. Nómada Tasques: la bateria completa del model

Ho ajuntem tot. Primer, una ajuda compartida per no repetir dades a cada fitxer:

// proves/ajudes/backlog-de-prova.js
import { Tasca } from '../../js/model/tasca.js';
import { Tauler } from '../../js/model/tauler.js';
import { crearBacklog } from '../../js/dades/backlog.js';

/** Data canònica del projecte: fixa, perquè les proves no depenguin del calendari. */
export const AVUI = '2026-09-20';

export const DADES_VALIDES = Object.freeze({
  id: 1, titol: 'Redissenyar la sala polivalent', responsable: 'Iván',
  prioritat: 'alta', estat: 'en-curs', etiquetes: ['espai', 'disseny'],
  horesEstimades: 12, dataLimit: '2026-09-30', revisor: 'Marta'
});

/** Una tasca vàlida a la qual es canvia el que interessi. */
export const unaTasca = (canvis = {}) => new Tasca({ ...DADES_VALIDES, ...canvis });

/** Un tauler NOU amb el backlog canònic. Instàncies fresques a cada crida. */
export const unTauler = () => new Tauler('Taller Nómada', crearBacklog());

/** Captura l'excepció de fn, o retorna null si no llança. */
export function capturar(fn) {
  try { fn(); return null; } catch (error) { return error; }
}

Ara les proves de Tasca:

// proves/model/tasca.test.js
import { Tasca } from '../../js/model/tasca.js';
import { ErrorDeValidacio } from '../../js/model/errors.js';
import { AVUI, DADES_VALIDES, unaTasca, capturar } from '../ajudes/backlog-de-prova.js';

describe('Tasca', () => {
  // ── Construcció i validació ───────────────────────────────────────────
  describe('construcció', () => {
    test('conserva les dades vàlides que rep', () => {
      const tasca = unaTasca();

      expect(tasca).toMatchObject({
        id: 1, titol: 'Redissenyar la sala polivalent',
        responsable: 'Iván', prioritat: 'alta', horesEstimades: 12
      });
    });

    test('retalla els espais del títol', () => {
      expect(unaTasca({ titol: '   Cartelleria   ' }).titol).toBe('Cartelleria');
    });

    test.each([['', 'cadena buida'], ['   ', 'només espais'], [null, 'null'], [42, 'un número']])(
      'rebutja el títol %p (%s) amb ErrorDeValidacio (R2)',
      (titol) => {
        expect(() => unaTasca({ titol })).toThrow(ErrorDeValidacio);
      }
    );

    test('el títol invàlid identifica el camp i el valor rebut', () => {
      const error = capturar(() => unaTasca({ titol: '  ' }));

      expect(error).toBeInstanceOf(ErrorDeValidacio);
      expect(error.camp).toBe('titol');
      expect(error.valorRebut).toBe('  ');
    });

    test('neix en pendent quan no es rep cap estat (R5)', () => {
      const { estat, ...senseEstat } = DADES_VALIDES;   // desestructuració de 04-07

      expect(new Tasca(senseEstat).estat).toBe('pendent');
    });

    test('rebutja un estat desconegut', () => {
      expect(() => unaTasca({ estat: 'arxivada' })).toThrow(ErrorDeValidacio);
    });

    test('una tasca sense responsable el desa com a null, mai com a cadena buida (R8)', () => {
      expect(unaTasca({ responsable: '' }).responsable).toBeNull();
      expect(unaTasca({ responsable: undefined }).responsable).toBeNull();
    });

    test('normalitza les etiquetes a minúscules i sense duplicats (R9)', () => {
      const tasca = unaTasca({ etiquetes: ['Espai', ' DISSENY ', 'espai', 'disseny'] });

      expect(tasca.etiquetes).toEqual(['espai', 'disseny']);
    });
  });

  // ── Hores (R3), amb els valors frontera ───────────────────────────────
  describe('horesEstimades (R3)', () => {
    test.each`
      hores   | valid    | motiu
      ${0}    | ${false} | ${'zero'}
      ${-3}   | ${false} | ${'negatiu'}
      ${1}    | ${true}  | ${'límit inferior'}
      ${40}   | ${true}  | ${'límit superior exacte'}
      ${41}   | ${false} | ${'supera el màxim'}
      ${NaN}  | ${false} | ${'NaN'}
      ${'12'} | ${false} | ${'cadena, no número'}
    `('$hores → vàlid: $valid ($motiu)', ({ hores, valid }) => {
      const crear = () => unaTasca({ horesEstimades: hores });

      if (valid) expect(crear().horesEstimades).toBe(hores);
      else expect(crear).toThrow(ErrorDeValidacio);
    });

    test('el setter valida igual que el constructor', () => {
      const tasca = unaTasca();

      expect(() => { tasca.horesEstimades = 99; }).toThrow(ErrorDeValidacio);
      expect(tasca.horesEstimades).toBe(12);        // i NO s'ha modificat
    });
  });

  // ── Transicions (R6) ──────────────────────────────────────────────────
  describe("transicions d'estat (R6)", () => {
    test.each([
      ['pendent', 'en-curs'], ['en-curs', 'feta'], ['en-curs', 'pendent']
    ])('permet %s → %s', (origen, desti) => {
      const tasca = unaTasca({ estat: origen });

      expect(tasca.canviarEstat(desti).estat).toBe(desti);   // retorna this: encadenable
    });

    test.each([
      ['pendent', 'pendent'], ['pendent', 'feta'], ['en-curs', 'en-curs'],
      ['feta', 'pendent'], ['feta', 'en-curs'], ['feta', 'feta']
    ])('prohibeix %s → %s', (origen, desti) => {
      expect(() => unaTasca({ estat: origen }).canviarEstat(desti)).toThrow(ErrorDeValidacio);
    });

    test('una transició invàlida no deixa la tasca a mitges', () => {
      const tasca = unaTasca({ estat: 'feta' });

      capturar(() => tasca.canviarEstat('pendent'));

      expect(tasca.estat).toBe('feta');             // l'estat no s'ha corromput
    });
  });

  // ── Venciment (R10) i derivats ────────────────────────────────────────
  describe('venciment (R10)', () => {
    test('vençuda si la data ha passat i no està feta', () => {
      expect(unaTasca({ dataLimit: '2026-09-05', estat: 'pendent' }).estaVencuda(AVUI)).toBe(true);
    });

    test('no vençuda si està feta, encara que la data hagi passat', () => {
      expect(unaTasca({ dataLimit: '2026-09-05', estat: 'feta' }).estaVencuda(AVUI)).toBe(false);
    });

    test('no vençuda el mateix dia del límit', () => {
      expect(unaTasca({ dataLimit: AVUI, estat: 'pendent' }).estaVencuda(AVUI)).toBe(false);
    });

    test('oberta és false només quan està feta', () => {
      expect(unaTasca({ estat: 'pendent' }).oberta).toBe(true);
      expect(unaTasca({ estat: 'en-curs' }).oberta).toBe(true);
      expect(unaTasca({ estat: 'feta' }).oberta).toBe(false);
    });

    test('esforç = hores × pes de prioritat', () => {
      expect(unaTasca({ prioritat: 'alta', horesEstimades: 12 }).esforc).toBe(36);
      expect(unaTasca({ prioritat: 'mitjana', horesEstimades: 6 }).esforc).toBe(12);
      expect(unaTasca({ prioritat: 'baixa', horesEstimades: 3 }).esforc).toBe(3);
    });
  });

  // ── Serialització (reprenent 04-08) ───────────────────────────────────
  describe('serialització', () => {
    test('toJSON inclou el camp privat estat, que altrament es perdria', () => {
      const tasca = unaTasca({ estat: 'en-curs' });

      expect(tasca.toJSON()).toStrictEqual({
        id: 1, titol: 'Redissenyar la sala polivalent', responsable: 'Iván',
        prioritat: 'alta', estat: 'en-curs', etiquetes: ['espai', 'disseny'],
        horesEstimades: 12, dataLimit: '2026-09-30', revisor: 'Marta'
      });
    });

    test('el cicle de serialització i reconstrucció conserva tots els camps', () => {
      const original = unaTasca({ estat: 'en-curs' });

      const copia = Tasca.desDeJSON(JSON.stringify(original));

      expect(copia.toJSON()).toStrictEqual(original.toJSON());
      expect(copia).not.toBe(original);                  // és UNA ALTRA instància
      expect(copia).toBeInstanceOf(Tasca);               // però de la mateixa classe
    });

    test("toJSON retorna una còpia de les etiquetes, no l'array intern", () => {
      const tasca = unaTasca();

      tasca.toJSON().etiquetes.push('injectada');

      expect(tasca.etiquetes).toEqual(['espai', 'disseny']);   // intacte
    });
  });
});

I les de Tauler, on per fi es comproven els números canònics:

// proves/model/tauler.test.js
import { Tasca } from '../../js/model/tasca.js';
import { ErrorDeValidacio } from '../../js/model/errors.js';
import { AVUI, unaTasca, unTauler } from '../ajudes/backlog-de-prova.js';

describe('Tauler', () => {
  let tauler;

  beforeEach(() => { tauler = unTauler(); });      // ← instàncies noves: proves independents

  describe('el backlog canònic', () => {
    test('té 6 tasques i 5 obertes', () => {
      expect(tauler.total).toBe(6);
      expect(tauler.obertes).toHaveLength(5);
    });

    test('suma 48 h totals i 45 h obertes', () => {
      expect(tauler.horesTotals).toBe(48);
      expect(tauler.horesObertes).toBe(45);        // ← la prova que faltava al cas 3 de 08-01
    });

    test('té un esforç ponderat de 124', () => {
      expect(tauler.esforc).toBe(124);
    });

    test('té exactament una tasca vençuda: el pressupost de la fusteria (R10)', () => {
      const vencudes = tauler.vencudes(AVUI);

      expect(vencudes).toHaveLength(1);
      expect(vencudes[0].titol).toBe('Pressupost de la fusteria');
    });

    test('reparteix les hores obertes: Iván 25, Lucía 14, Marta 6', () => {
      expect(tauler.horesPerResponsable()).toEqual({ 'Iván': 25, 'Lucía': 14, 'Marta': 6 });
    });

    test('el resum complet coincideix amb el valor esperat', () => {
      expect(tauler.resum(AVUI)).toStrictEqual({
        total: 6, obertes: 5, horesTotals: 48,
        horesObertes: 45, vencudes: 1, esforc: 124
      });
    });
  });

  describe('afegir', () => {
    test('afegeix una tasca nova i actualitza el total', () => {
      tauler.afegir(unaTasca({ id: 7, titol: 'Revisar els extintors', horesEstimades: 2 }));

      expect(tauler.total).toBe(7);
      expect(tauler.cercarPerId(7).titol).toBe('Revisar els extintors');
    });

    test('rebutja un id duplicat (R1)', () => {
      expect(() => tauler.afegir(unaTasca({ id: 3 }))).toThrow(ErrorDeValidacio);
      expect(tauler.total).toBe(6);                 // i no ha afegit res
    });

    test('rebutja qualsevol cosa que no sigui una Tasca', () => {
      expect(() => tauler.afegir({ id: 9, titol: 'Objecte pla' })).toThrow(ErrorDeValidacio);
    });
  });

  describe('canviarEstat', () => {
    test('aplica la transició a la tasca indicada', () => {
      tauler.canviarEstat(2, 'en-curs');

      expect(tauler.cercarPerId(2).estat).toBe('en-curs');
    });

    test('recalcula les hores obertes en tancar una tasca', () => {
      tauler.canviarEstat(1, 'feta');               // tasca 1: en-curs, 12 h, de l'Iván

      expect(tauler.horesObertes).toBe(33);         // 45 − 12
      expect(tauler.horesPerResponsable()).toEqual({ 'Iván': 13, 'Lucía': 14, 'Marta': 6 });
    });

    test('llança ErrorDeValidacio si la tasca no existeix', () => {
      expect(() => tauler.canviarEstat(99, 'en-curs')).toThrow(ErrorDeValidacio);
    });

    test("propaga l'error d'una transició prohibida (R6)", () => {
      expect(() => tauler.canviarEstat(4, 'en-curs')).toThrow(ErrorDeValidacio);  // la 4 està feta
    });
  });

  describe('consultes', () => {
    test('cercarPerId retorna null si no existeix, mai undefined', () => {
      expect(tauler.cercarPerId(99)).toBeNull();
    });

    test('filtrar retorna un array nou sense tocar el tauler', () => {
      const altes = tauler.filtrar((t) => t.prioritat === 'alta');

      expect(altes).toHaveLength(3);
      expect(tauler.total).toBe(6);
    });

    test('el getter tasques retorna una còpia defensiva (05-03)', () => {
      const copia = tauler.tasques;

      copia.push(unaTasca({ id: 99 }));
      copia.sort((a, b) => a.titol.localeCompare(b.titol, 'ca'));

      expect(tauler.total).toBe(6);                 // el push no ha afectat
      expect(tauler.tasques[0].id).toBe(1);         // ni el sort
    });

    test('és iterable: for…of recorre les sis tasques (05-08)', () => {
      const titols = [...tauler].map((t) => t.titol);

      expect(titols).toHaveLength(6);
      expect(titols[0]).toBe('Redissenyar la sala polivalent');
    });
  });
});
$ npm test

 PASS  proves/model/tasca.test.js
 PASS  proves/model/tauler.test.js

Test Suites: 2 passed, 2 total
Tests:       48 passed, 48 total
Time:        0.913 s

Quaranta-vuit comprovacions del comportament de tot el model, en menys d'un segon, sense navegador. I el deute del cas 3 de 08-01 queda formalment saldat: si algú torna a escriure this.#tasques.reduce a horesObertes, dues proves es posen vermelles a l'instant i una d'elles diu literalment «suma 48 h totals i 45 h obertes».

L'últim pas: afegir les proves al flux d'integració contínua de 08-02.

      # .github/workflows/qualitat.yml — un pas més
      - name: Executar les proves
        run: npm test -- --coverage --ci

--ci desactiva l'escriptura automàtica d'instantànies noves; al servidor, una instantània que no existeix ha de fallar, no crear-se en silenci.

Errors Habituals i Consells

  • Fer servir toBe amb objectes o arrays. Compara referències: expect({a:1}).toBe({a:1}) sempre falla. Per a contingut, toEqual o toStrictEqual.
  • Oblidar la funció a toThrow. expect(new Tasca({})) llança abans que expect faci res. Sempre expect(() => …).toThrow(…).
  • Proves asíncrones sense async/await ni return. La prova acaba abans que es comprovi res i passa sempre. És un fals verd, el resultat més perillós de tots.
  • Oblidar l'await abans d'expect(...).resolves. Mateix problema, mateix fals verd.
  • Compartir estat entre proves. Un objecte creat a l'àmbit del mòdul fa que l'ordre importi i apareguin fallades intermitents. Sempre beforeEach.
  • Deixar-se un test.only posat. La suite passa en verd executant una sola prova. És exactament el que caça jest/no-focused-tests, configurat a 08-02.
  • Provar detalls interns en lloc de comportament. Una prova que comprova tauler.#tasques (si pogués) es trencaria a cada refactorització sense que res estigui malament. Prova el que el mòdul promet, no com ho compleix.
  • Perseguir el 100 % de cobertura. Els últims punts costen molt i valen poc. Exigeix molt al model, sigues raonable a la vista.
  • Fer servir la data real a les proves. Una prova que crida new Date() canvia de resultat l'1 d'octubre. Passa AVUI = '2026-09-20' com a argument; per al codi que no ho permet, hi ha els temporitzadors falsos de 08-04.
  • Consell: escriu primer la prova que reprodueix l'error. És el pas 6 del mètode de 08-01 i l'únic cas en què TDD és indiscutible.
  • Consell: comprova que la prova falla abans d'arreglar res. Una prova que mai has vist en vermell pot no estar comprovant el que creus.
  • Consell: quan una prova sigui difícil d'escriure, sospita del codi, no de la prova. La dificultat gairebé sempre assenyala una funció que fa massa coses.

Exercicis

Exercici 1 — Bateria de js/util/dates.js. Escriu proves/util/dates.test.js amb la cobertura completa de les tres funcions del mòdul. Per a diesEntre, inclou dies positius, negatius, zero i un salt de mes. Per a estaVencuda, fes servir test.each amb una taula que cobreixi les quatre combinacions de (data passada/futura) × (estat feta/no feta), més el cas del mateix dia. Per a dataLlegible, comprova almenys gener, setembre i desembre, i el dia sense zero al davant. Justifica en un comentari per què cap prova fa servir new Date() sense arguments.

Exercici 2 — Regla nova amb TDD: R12, el límit setmanal. La Marta demana implementar per fi R7 com a mètode del tauler: potAssignar(responsable, hores, avui) ha de retornar false si assignar aquestes hores faria que aquella persona superés 40 h obertes. Desenvolupa'l amb el cicle vermell-verd-refactor, escrivint cada prova abans que el codi, i documenta les quatre voltes. Cobreix: (a) l'Iván té 25 h obertes, així que acceptar 10 h més està permès; (b) acceptar 16 h més no ho està; (c) el límit exacte (25 + 15 = 40) sí que es permet; (d) les tasques fetes no compten; (e) una persona sense tasques pot acceptar fins a 40 h.

Exercici 3 — Diagnosticar una suite malalta. Aquesta suite té cinc problemes diferents. Identifica'ls tots, explica quina conseqüència té cadascun i reescriu-la correctament.

import { Tauler } from '../../js/model/tauler.js';
import { crearBacklog } from '../../js/dades/backlog.js';

const tauler = new Tauler('Taller Nómada', crearBacklog());

describe('tauler', () => {
  test('funciona', () => {
    expect(tauler.total).toBe(6);
    tauler.canviarEstat(2, 'en-curs');
    expect(tauler.cercarPerId(2).estat).toBe('en-curs');
  });

  test('resum', () => {
    const r = tauler.resum(new Date().toISOString().slice(0, 10));
    expect(r).toBe({ total: 6, obertes: 5, horesTotals: 48,
                     horesObertes: 45, vencudes: 1, esforc: 124 });
  });

  test('càrrega', () => {
    carregarDesDelServidor().then((t) => {
      expect(t.total).toBe(6);
    });
  });

  test.only('id duplicat', () => {
    expect(() => tauler.afegir(tauler.tasques[0])).toThrow();
  });
});

Solucions

Solució 1

// proves/util/dates.test.js
import { AVUI, diesEntre, estaVencuda, dataLlegible } from '../../js/util/dates.js';

// Cap prova crida new Date() sense arguments: si ho fes, el resultat dependria
// del dia d'execució i la suite es trencaria sola un dimarts qualsevol sense que
// ningú hagués tocat el codi. El temps entra SEMPRE com a dada.

describe('AVUI', () => {
  test('és la data canònica del projecte en format ISO', () => {
    expect(AVUI).toBe('2026-09-20');
    expect(AVUI).toMatch(/^\d{4}-\d{2}-\d{2}$/);
  });
});

describe('diesEntre', () => {
  test.each`
    inici             | fi                | esperat  | cas
    ${'2026-09-20'}   | ${'2026-09-30'}   | ${10}    | ${'deu dies endavant'}
    ${'2026-09-20'}   | ${'2026-09-05'}   | ${-15}   | ${'quinze dies enrere'}
    ${'2026-09-20'}   | ${'2026-09-20'}   | ${0}     | ${'el mateix dia'}
    ${'2026-09-20'}   | ${'2026-10-02'}   | ${12}    | ${'canvi de mes'}
    ${'2026-12-28'}   | ${'2027-01-03'}   | ${6}     | ${"canvi d'any"}
    ${'2028-02-27'}   | ${'2028-03-01'}   | ${3}     | ${'any de traspàs'}
  `('$cas: $inici → $fi = $esperat', ({ inici, fi, esperat }) => {
    expect(diesEntre(inici, fi)).toBe(esperat);
  });
});

describe('estaVencuda (R10)', () => {
  test.each`
    data              | estat           | esperat  | motiu
    ${'2026-09-05'}   | ${'pendent'}    | ${true}  | ${'passada i sense començar'}
    ${'2026-09-05'}   | ${'en-curs'}    | ${true}  | ${'passada i en curs'}
    ${'2026-09-05'}   | ${'feta'}       | ${false} | ${'passada però acabada'}
    ${'2026-10-30'}   | ${'pendent'}    | ${false} | ${'futura i sense començar'}
    ${'2026-10-30'}   | ${'feta'}       | ${false} | ${'futura i acabada'}
    ${'2026-09-20'}   | ${'pendent'}    | ${false} | ${'venç avui: encara hi ha temps'}
  `('$data + $estat → $esperat ($motiu)', ({ data, estat, esperat }) => {
    expect(estaVencuda(data, estat, '2026-09-20')).toBe(esperat);
  });

  test('fa servir AVUI per defecte si no es passa data de referència', () => {
    expect(estaVencuda('2026-09-05', 'pendent')).toBe(true);
    expect(estaVencuda('2026-12-01', 'pendent')).toBe(false);
  });
});

describe('dataLlegible', () => {
  test.each([
    ['2026-01-01', '1 de gener de 2026'],
    ['2026-09-05', '5 de setembre de 2026'],
    ['2026-09-20', '20 de setembre de 2026'],
    ['2026-12-31', '31 de desembre de 2026']
  ])('%s → %s', (iso, esperat) => {
    expect(dataLlegible(iso)).toBe(esperat);
  });

  test('treu el zero inicial del dia', () => {
    expect(dataLlegible('2026-03-08')).toBe('8 de març de 2026');
    expect(dataLlegible('2026-03-08')).not.toContain('08');
  });
});

Solució 2

VOLTA 1 · VERMELL — el cas permès
test('Iván, amb 25 h obertes, pot acceptar 10 h més (R7)', () => {
  expect(unTauler().potAssignar('Iván', 10)).toBe(true);
});
// ✗ TypeError: tauler.potAssignar is not a function
VOLTA 1 · VERD — el mínim
potAssignar() { return true; }
VOLTA 2 · VERMELL — el cas prohibit
test('Iván, amb 25 h obertes, NO pot acceptar 16 h més (25 + 16 = 41 > 40)', () => {
  expect(unTauler().potAssignar('Iván', 16)).toBe(false);
});
// ✗ Expected: false   Received: true
VOLTA 2 · VERD
potAssignar(responsable, hores) {
  return (this.horesPerResponsable()[responsable] ?? 0) + hores <= 40;
}
VOLTA 3 · VERMELL — el límit exacte i qui no té res assignat
test('el límit exacte de 40 h està permès (25 + 15)', () => {
  expect(unTauler().potAssignar('Iván', 15)).toBe(true);
});

test('una persona sense tasques pot acceptar fins a 40 h', () => {
  const tauler = unTauler();

  expect(tauler.potAssignar('Berta', 40)).toBe(true);
  expect(tauler.potAssignar('Berta', 41)).toBe(false);
});
// Totes dues passen: el <= i el ?? 0 ja estaven bé. Verd a la primera,
// però valia la pena escriure-les: documenten les vores.

VOLTA 4 · VERMELL — les tasques fetes no compten
test('les tasques fetes no consumeixen quota setmanal', () => {
  const tauler = unTauler();

  tauler.canviarEstat(1, 'feta');        // l'Iván tanca 12 h: li queden 13 obertes

  expect(tauler.potAssignar('Iván', 27)).toBe(true);    // 13 + 27 = 40
  expect(tauler.potAssignar('Iván', 28)).toBe(false);   // 13 + 28 = 41
});
// Passa, perquè horesPerResponsable ja filtra per `obertes`. La prova
// documenta i protegeix aquest detall, que és fàcil de trencar en un refactor.
REFACTOR — amb les cinc proves en verd
// js/model/tauler.js
const MAXIM_SETMANAL = 40;                      // R7, en una constant amb nom

/**
 * R7: ningú pot superar 40 h estimades OBERTES.
 * Les tasques fetes no consumeixen quota.
 *
 * @param {string} responsable
 * @param {number} hores  Hores que es pretenen assignar
 * @returns {boolean}
 */
potAssignar(responsable, hores) {
  const assignades = this.horesPerResponsable()[responsable] ?? 0;
  return assignades + hores <= MAXIM_SETMANAL;
}

/** Hores que encara es poden assignar a algú sense incomplir R7. */
quotaDisponible(responsable) {
  return MAXIM_SETMANAL - (this.horesPerResponsable()[responsable] ?? 0);
}

Solució 3

# Problema Conseqüència
1 Estat compartit: el tauler es crea una vegada fora dels describe La primera prova tanca la tasca 2 i contamina les altres; l'ordre passa a importar i apareixen fallades intermitents
2 Noms inútils ('tauler', 'funciona', 'resum', 'càrrega') i diversos motius de fallada per prova Quan CI es posa vermella, el nom no diu res i cal obrir el fitxer
3 toBe amb un objecte a la prova del resum Falla sempre: compara referències. Ha de ser toStrictEqual
4 Data real amb new Date() La prova canvia de resultat segons el dia: l'1 d'octubre hi haurà 2 vençudes i fallarà sola
5 Prova asíncrona sense async/await i test.only oblidat La de carregarDesDelServidor passa sempre sense comprovar res (fals verd), i el .only deixa la resta de la suite sense executar
// Suite corregida
import { Tauler } from '../../js/model/tauler.js';
import { ErrorDeValidacio } from '../../js/model/errors.js';
import { crearBacklog } from '../../js/dades/backlog.js';
import { carregarDesDelServidor } from '../../js/dades/api-tasques.js';

const AVUI = '2026-09-20';                    // ← data fixa: el temps entra com a dada

describe('Tauler', () => {
  let tauler;

  beforeEach(() => {                          // ← estat nou per a cada prova
    tauler = new Tauler('Taller Nómada', crearBacklog());
  });

  test('el backlog canònic té 6 tasques', () => {
    expect(tauler.total).toBe(6);
  });

  test('canviarEstat aplica la transició a la tasca indicada', () => {
    tauler.canviarEstat(2, 'en-curs');

    expect(tauler.cercarPerId(2).estat).toBe('en-curs');
  });

  test('el resum del backlog canònic coincideix amb el valor esperat', () => {
    expect(tauler.resum(AVUI)).toStrictEqual({   // ← toStrictEqual, no toBe
      total: 6, obertes: 5, horesTotals: 48,
      horesObertes: 45, vencudes: 1, esforc: 124
    });
  });

  test('afegir rebutja un id duplicat (R1)', () => {   // ← sense .only
    expect(() => tauler.afegir(tauler.tasques[0])).toThrow(ErrorDeValidacio);
    expect(tauler.total).toBe(6);
  });

  test('carregarDesDelServidor retorna les 6 tasques del backlog', async () => {
    const carregat = await carregarDesDelServidor();   // ← async + await

    expect(carregat.total).toBe(6);
  });
  // Nota: aquesta última prova encara depèn de la xarxa de debò, cosa que la fa
  // lenta i no determinista. A 08-04 se substitueix fetch per un doble i passa a
  // executar-se en mil·lisegons, sense sortir de la màquina.
});

Conclusió

Nómada Tasques té per fi una xarxa de seguretat. Saps què és una prova automatitzada —un programa que executa un altre programa i comprova el resultat— i què hi guanya de debò l'equip: poder refactoritzar sense por, una documentació executable que no pot mentir perquè falla quan deixa de ser certa, i una pressió constant cap a un disseny millor, perquè el que és difícil de provar sol estar mal dissenyat. Coneixes la piràmide de proves amb els seus tres nivells, el perquè de la seva forma —la confiança i el cost pugen junts— i l'antipatró del con de gelat.

Tens Jest funcionant sobre mòduls ES natius, amb la bandera de Node al script i un jest.config.js sense transformacions, de manera que el que es prova és exactament el que es desplega; i coneixes l'alternativa amb Babel i les seves contrapartides. Domines l'anatomia d'una prova: describe per agrupar, test per comprovar, i el patró AAA —Preparar, Actuar una sola vegada, Comprovar—. Saps triar matcher: toBe per a primitius i per a identitat deliberada, toEqual per a contingut, toStrictEqual quan el tipus de classe i els undefined importen —com a la sortida de toJSON(), pel parany de 04-08—, toMatchObject per no lligar-te a camps que no vénen al cas, i toThrow sempre amb una funció a dins, preferiblement comprovant el tipus i no el text del missatge, amb expect.assertions protegint els try/catch perquè una fallada no es disfressi de verd.

Saps fer servir els ganxos i per què beforeEach és el més utilitzat; entens que l'estat compartit és la causa número u de les fallades intermitents i que aquella decisió de 05-04 d'exportar crearBacklog() com a funció en lloc d'un array ja construït és avui el que fa independents les teves 48 proves. Cobreixes la taula completa de transicions R6 amb test.each en les seves dues sintaxis, i els valors frontera de R3 —0, 1, 40, 41, NaN, '12'— en una taula que es llegeix com una especificació. Saps provar codi asíncron amb async/await, resolves i rejects, i reconèixer el fals verd d'una prova asíncrona que ningú espera.

I sobretot, s'ha tancat la promesa de 03-03: entens què fa provable un mòdul. Una funció pura com estaVencuda(data, estat, avui) es prova en una línia perquè el temps entra com a dada; marcarVencudes(), que llegeix el rellotge, el magatzem i el DOM, necessita mig Mòdul 8 per provar-se. La injecció de dependències que portes aplicant sense anomenar-la —l'avui = AVUI d'estaVencuda, el magatzem del constructor de RepositoriLocal— és exactament el que fa substituïble una dependència en una prova. Saps llegir la cobertura, mirar la columna branch abans que cap altra, posar llindars per carpeta (exigents al model, raonables a la vista) i no confondre executar amb verificar. Anomenes les proves amb subjecte, comportament i condició, cites la regla de negoci entre parèntesis, i respectes la regla del motiu únic de fallada. I has fet TDD de debò en tres voltes de vermell-verd-refactor sobre R11, comprovant que el vermell arriba pel motiu correcte abans d'escriure una línia.

El resultat són 48 proves en menys d'un segon que blinden Tasca i Tauler complets: les validacions R2, R3, R5, R8 i R9; les nou combinacions de R6; el venciment R10 amb el seu cas del mateix dia; el cicle de toJSON/desDeJSON; les còpies defensives de 05-03; la iterabilitat de 05-08; i els números canònics del backlog —6 tasques, 48 h totals, 45 h obertes, esforç 124, una vençuda, Iván 25 / Lucía 14 / Marta 6—. El cas 3 de 08-01 no pot tornar.

Però fixa't en què s'ha quedat fora. js/dades/repositori-local.js apareix a l'informe de cobertura al 31 %, i no és per mandra: per provar-lo cal un localStorage que a Node no existeix. js/dades/api-tasques.js no té ni una prova, perquè provar-lo de debò significaria cridar un servidor —lent, poc fiable i amb un TLD .example que no resol mai—. El debounce de js/util/temps.js esperaria 300 ms reals per prova, i els reintents amb retrocés exponencial de js/dades/http.js trigarien gairebé dos segons cadascun. I estaVencuda() només és comprovable perquè vam tenir la precaució de passar-li avui com a paràmetre; el dia que algú cridi new Date() a dins, la suite començarà a fallar sola els dimarts. Xarxa, rellotge, emmagatzematge i aleatorietat: quatre dependències que fan les proves lentes i no deterministes, que és la definició exacta d'una suite que ningú executa. La solució és substituir-les per peces controlades, i això és Dobles de Prova: Mocks, Stubs i Spies, on provaràs les quatre capes restants sense xarxa, sense esperes i amb el calendari congelat al 20 de setembre de 2026.

Curs de JavaScript: De Principiant a Avançat

Mòdul 1: Introducció a JavaScript

Mòdul 2: Estructures de Control

Mòdul 3: Funcions

Mòdul 4: Objectes i Arrays

Mòdul 5: Objectes i Funcions Avançades

Mòdul 6: El Model d'Objectes del Document (DOM)

Mòdul 7: APIs del Navegador i Temes Avançats

Mòdul 8: Proves i Depuració

Mòdul 9: Rendiment i Optimització

Mòdul 10: Frameworks i Llibreries de JavaScript

Mòdul 11: Projecte Final

© Copyright 2026. Tots els drets reservats