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
- Què és una prova automatitzada
- Què hi guanya l'equip: tres beneficis i cap no és «detectar bugs»
- La piràmide de proves
- Jest: instal·lació i primera execució
- Configuració per a mòduls ES
- Anatomia d'una prova:
describe,testi el patró AAA - Els matchers que es fan servir de debò
toBe,toEqualitoStrictEqual: la comparació per referència- Comprovar que alguna cosa llança un error
- Ganxos:
beforeEachi companyia - L'estat compartit, font de fallades intermitents
- Proves parametritzades amb
test.each - Provar codi asíncron
- Què fa provable un mòdul
- Cobertura: què mesura de debò
- Anomenar una prova i la regla del motiu únic
- TDD: vermell, verd, refactor
- Nómada Tasques: la bateria completa del model
- Errors Habituals i Consells
- Exercicis
- Conclusió
- 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"]
- 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.
- 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.
- 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).
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 dinsEls 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 sEl mode watch és on Jest canvia la manera de treballar:
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 |
- 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».
- Anatomia d'una prova:
describe, test i el patró AAA
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'escriureit('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.
- 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é siobertaretorna'si',1o[]. Si esperes un booleà, escriutoBe(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.
toBe, toEqual i toStrictEqual: la comparació per referència
toBe, toEqual i toStrictEqual: la comparació per referènciaAquest é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 objectetoBe 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]); // ✗ fallaLa 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 sí 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.
- 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ó regularRecomanació: 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.
- Ganxos:
beforeEach i companyia
beforeEach i companyiaLes 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
- 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.
- Proves parametritzades amb
test.each
test.eachLa 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 fetaNou 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).
- 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.
- 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
- Cobertura: què mesura de debò
La cobertura mesura quin percentatge del teu codi s'executa en passar les proves.
-------------------------|---------|----------|---------|---------|------------------- 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.jsestà 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.
- 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
});
- 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');
});Volta 2 · Verd.
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.
- 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 sQuaranta-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
toBeamb objectes o arrays. Compara referències:expect({a:1}).toBe({a:1})sempre falla. Per a contingut,toEqualotoStrictEqual. - Oblidar la funció a
toThrow.expect(new Tasca({}))llança abans queexpectfaci res. Sempreexpect(() => …).toThrow(…). - Proves asíncrones sense
async/awaitnireturn. 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'
awaitabans 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.onlyposat. La suite passa en verd executant una sola prova. És exactament el que caçajest/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. PassaAVUI = '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
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 functiontest('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: truepotAssignar(responsable, hores) {
return (this.horesPerResponsable()[responsable] ?? 0) + hores <= 40;
}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 comptentest('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.// 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
- Què és JavaScript?
- Configuració del teu Entorn de Desenvolupament
- El teu Primer Programa en JavaScript
- Sintaxi i Conceptes Bàsics de JavaScript
- Variables i Tipus de Dades
- Operadors Bàsics
- Conversió de Tipus i Comparacions
- El Projecte del Curs: Nómada Tasques
Mòdul 2: Estructures de Control
- Sentències Condicionals
- Bucles: for, while, do-while
- Sentències Switch
- Control del Flux: break, continue i Bucles Imbricats
- Gestió d'Errors amb try-catch
Mòdul 3: Funcions
- Definició i Crida de Funcions
- Expressions de Funció i Funcions Fletxa
- Paràmetres i Valors de Retorn
- Àmbit i Closures
- Hoisting i el Context d'Execució
- Funcions d'Ordre Superior
- Recursivitat
Mòdul 4: Objectes i Arrays
- Introducció als Objectes
- Mètodes d'Objecte i la Paraula Clau
this - Arrays: Conceptes Bàsics i Mètodes
- Iteració sobre Arrays
- Cercar, Ordenar i Agregar Dades: find, sort i reduce
- Desestructuració d'Arrays
- Desestructuració d'Objectes, Spread i Rest
- JSON i Còpies d'Objectes
Mòdul 5: Objectes i Funcions Avançades
- Prototips i Herència
- Classes i Programació Orientada a Objectes
- Encapsulació: Getters, Setters i Camps Privats
- Mòduls i Importació/Exportació
- JavaScript Asíncron: Callbacks
- Promeses i Async/Await
- El Bucle d'Esdeveniments i la Cua de Microtasques
- Iteradors i Generadors
Mòdul 6: El Model d'Objectes del Document (DOM)
- Introducció al DOM
- Selecció i Manipulació d'Elements del DOM
- Gestió d'Esdeveniments
- Propagació, Delegació i Esdeveniments Personalitzats
- Creació i Eliminació d'Elements del DOM
- Renderitzat de Llistes i Plantilles HTML
- Gestió i Validació de Formularis
Mòdul 7: APIs del Navegador i Temes Avançats
- Emmagatzematge Local i de Sessió
- Fetch API i AJAX
- Peticions Robustes: Errors, Timeouts i AbortController
- WebSockets
- Service Workers i Aplicacions Web Progressives (PWAs)
- APIs del Navegador Essencials
- Introducció a WebAssembly
Mòdul 8: Proves i Depuració
- Depuració de JavaScript
- Qualitat de Codi: ESLint, Prettier i Convencions
- Proves Unitàries amb Jest
- Dobles de Prova: Mocks, Stubs i Spies
- Proves d'Integració
- Proves d'Extrem a Extrem amb Cypress
Mòdul 9: Rendiment i Optimització
- Mesurar Abans d'Optimitzar: DevTools i Web Vitals
- Optimització del Rendiment de JavaScript
- Gestió de Memòria
- Manipulació Eficient del DOM
- Càrrega Diferida i Divisió de Codi
Mòdul 10: Frameworks i Llibreries de JavaScript
- Per Què Existeixen els Frameworks
- Introducció a React
- Gestió d'Estat amb Redux
- Conceptes Bàsics de Vue.js
- Conceptes Bàsics d'Angular
- Triar el Framework Adequat
