El mòdul anterior va acabar amb una confessió incòmoda: nou refactoritzacions sobre codi que funcionava —la signatura de LlistaBicicletes, l'estat mogut de lloc, el value de dos proveïdors de context reescrit, les rutes partides en nou fragments, una capa de càrrega mandrosa amb els seus fallats de xarxa— i una única comprovació en totes elles: obrir el navegador i mirar. Va funcionar perquè el projecte és petit i perquè la persona que tocava el codi recordava què calia mirar. Cap d'aquestes dues condicions sobreviu al creixement. Aquest mòdul construeix el que faltava: una xarxa de seguretat automàtica que s'executa en segons, que comprova el que un humà no pot recordar i que falla quan el comportament canvia. Aquesta primera lliçó encara no escriu proves de l'aplicació: estableix el criteri. Què és una prova, quins tipus n'hi ha, quins compensen en una interfície de React, què val la pena provar de CicloUrbano i què no, quines són les mètriques que enganyen, i com es deixa l'entorn a punt perquè les quatre lliçons següents escriguin codi de prova des de la primera línia.

Contingut

  1. El cost real de refactoritzar sense proves
  2. Què és una prova automatitzada: preparar, actuar, comprovar
  3. Les tres coses que aporta una prova
  4. Tipus de prova: estàtica, unitària, d'integració i d'extrem a extrem
  5. La piràmide de proves enfront del trofeu de proves
  6. El principi rector del mòdul: comportament, no implementació
  7. Què val la pena provar a CicloUrbano i què no
  8. Falsos amics: cobertura, proves fràgils i proves inestables
  9. Com s'anomena una prova
  10. Posada en marxa de l'entorn: Vitest, jsdom i Testing Library
  11. La primera prova: verificar que l'entorn respira
  12. L'estratègia del Mòdul 9

  1. El cost real de refactoritzar sense proves

Recupera el tancament del mòdul 8 i pensa en el que va passar realment a cadascuna d'aquelles nou refactoritzacions. A cadascuna hi va haver un moment, després de desar el fitxer, en què l'estat del projecte era desconegut. Es va resoldre mirant la pantalla. Mirar la pantalla comprova, com a molt, el que hi ha en pantalla en aquell instant: el catàleg amb les cinc bicicletes i el filtre per tipus. No comprova el que hi havia a l'altra pestanya, ni el cas de la reserva amb zero hores, ni què passa quan json-server retorna un 500, ni si el botó «Reservar» continua deshabilitat per a bici-003, que és en manteniment.

Això es pot formular amb precisió. En tocar codi, la pregunta és sempre la mateixa:

Quins comportaments podria haver trencat aquest canvi, i com sé que no els he trencat?

Sense proves, la resposta a la segona meitat és «no ho sé», i l'equip la substitueix per dues estratègies, totes dues dolentes:

  • No tocar el codi. El fitxer que ningú s'atreveix a modificar acaba sent el que més ho necessita. El deute tècnic no és el codi dolent: és el codi dolent que no es pot canviar.
  • Tocar-lo i esperar. El fallat arriba a l'usuari, que ho explica dies després, quan ja ningú recorda el canvi que ho va causar.

Una prova automatitzada converteix «no ho sé» en «ho comprovo en quatre segons». Aquesta és la diferència, i és una diferència de grau tan gran que canvia com s'escriu el codi: amb xarxa, es refactoritza sovint i en passos petits; sense xarxa, s'acumula i es reescriu de cop.

flowchart LR
    A["Canvi al codi"] --> B{"Hi ha proves?"}
    B -- No --> C["Comprovació manual parcial"]
    C --> D["Regressió no detectada"]
    D --> E["L'usuari informa de la fallada"]
    E --> F["Depuració sense context: dies després"]
    B -- Sí --> G["npm test · segons"]
    G --> H{"Falla alguna cosa?"}
    H -- Sí --> I["Es corregeix amb el canvi encara fresc"]
    H -- No --> J["S'integra amb confiança"]

L'eix horitzontal d'aquest diagrama és temps, i és on és l'estalvi: el cost d'arreglar una fallada creix amb la distància entre el moment en què s'introdueix i el moment en què es detecta.

  1. Què és una prova automatitzada: preparar, actuar, comprovar

Una prova automatitzada és, sense cap màgia, una funció que executa codi de l'aplicació i llança un error si el resultat no és l'esperat. Res més. L'executor de proves s'encarrega de trobar aquestes funcions, cridar-les, capturar els errors i presentar un informe.

Tota prova, sigui del tipus que sigui, té tres parts. A la literatura anglosaxona es coneixen com Arrange, Act, Assert (AAA); en aquest curs les anomenarem preparar, actuar i comprovar.

Part Què fa Exemple a CicloUrbano
Preparar Construeix l'escenari: dades d'entrada, estat inicial, dependències simulades Un catàleg amb bici-001 disponible i bici-003 en manteniment
Actuar Executa una sola acció: cridar la funció, prémer el botó, enviar el formulari Cridar validarReserva({ bicicletaId: 'bici-003', … }, catalog)
Comprovar Afirma què hauria d'haver passat El resultat conté un error a bicicletaId amb el text «no està disponible»
// Estructura de qualsevol prova, amb les tres parts marcades
test('rebutja una bicicleta en manteniment', () => {
  // 1. PREPARAR
  const bicicletes = [{ id: 'bici-003', model: 'Carga Max', estat: 'mantenimiento' }];
  const dades = { bicicletaId: 'bici-003', dataInici: '2030-01-01T10:00', hores: 2, condicions: true };

  // 2. ACTUAR
  const errors = validarReserva(dades, bicicletes);

  // 3. COMPROVAR
  expect(errors.bicicletaId).toContain('no està disponible');
});

Tres regles que surten directament d'aquesta estructura i que convé interioritzar abans d'escriure res:

  • Una prova, una acció. Si al bloc d'actuar hi ha tres coses, quan la prova falli no sabràs quina de les tres l'ha trencat. Divideix.
  • La preparació ha de ser explícita i local. Una dada que ve d'un altre fitxer, d'una prova anterior o d'un estat compartit converteix la fallada en un misteri. Cada prova construeix el seu escenari.
  • Comprovar el resultat, no els passos. La prova de dalt no comprova que s'hagi cridat bicicletes.find; comprova què retorna la funció. Això és el principi de l'apartat 6, en miniatura.

  1. Les tres coses que aporta una prova

Val la pena separar els tres beneficis, perquè cadascun justifica un tipus de prova diferent i explica decisions que es prenen més endavant.

3.1. Permet refactoritzar sense por

És el benefici principal i el que motiva aquest mòdul. Una refactorització, per definició, canvia la implementació sense canviar el comportament. Si tens una bateria de proves que descriu el comportament i no la implementació, refactoritzar es converteix en un procediment mecànic: canvies el codi, executes les proves, i si totes passen, la refactorització és correcta.

Aplicat al que ja vas fer: si LlistaBicicletes hagués tingut una prova que digués «donades cinc bicicletes, pinta cinc targetes, i en prémer “Reservar” a la primera avisa amb bici-001», el canvi de signatura i la introducció d'useCallback haurien quedat verificats a l'acte. I si el memo hagués deixat de propagar un camp, la prova hauria fallat.

La contrapartida és a la lletra petita: només funciona si la prova no depèn de la implementació. Una prova que comprova noms d'estat intern o nombre de renders es trenca amb cada refactorització, i llavors la xarxa de seguretat es converteix en un llast. D'aquí ve la insistència de l'apartat 6.

3.2. És documentació executable

Un fitxer de proves ben escrit respon a la pregunta «què fa exactament, això?» millor que qualsevol comentari, per una raó senzilla: els comentaris menteixen amb el temps i les proves no, perquè si menteixen, fallen.

✓ validarReserva
  ✓ retorna un objecte buit quan totes les dades són correctes
  ✓ exigeix triar una bicicleta
  ✓ rebutja una bicicleta que no existeix al catàleg
  ✓ rebutja una bicicleta en manteniment
  ✓ rebutja una data d'inici anterior a ara
  ✓ rebutja 0 hores i accepta 1
  ✓ rebutja 25 hores i accepta 24
  ✓ exigeix acceptar les condicions

Aquesta sortida és l'especificació de les regles de negoci de les reserves de CicloUrbano, generada pel propi codi. Qui entri nou a l'equip la llegeix en trenta segons.

3.3. Detecta regressions abans que l'usuari

Una regressió és un comportament que funcionava i ha deixat de funcionar. És el tipus de fallada més car, perquè ningú el busca: es descobreix per accident, normalment en producció i normalment a la part de l'aplicació en què ningú estava treballant.

Les proves inverteixen aquesta dinàmica: cada comportament que es prova una vegada queda vigilat per sempre, sense cost marginal. És l'única manera que una aplicació pugui créixer sense que la superfície de risc creixi amb ella.

  1. Tipus de prova: estàtica, unitària, d'integració i d'extrem a extrem

No totes les proves costen ni aporten el mateix. Aquesta taula és el mapa del mòdul sencer, i convé tornar-hi en començar cada lliçó.

Tipus Què prova Velocitat Cost de manteniment Confiança que aporta Què detecta que les altres no
Estàtica (ESLint, TypeScript) El codi sense executar-lo: sintaxi, tipus, regles Instantània (mentre escrius) Molt baix Baixa però gratuïta Errates, variables no usades, hooks mal cridats, props inexistents
Unitària Una funció o un mòdul aïllat Molt alta (mil·lisegons) Baix si el mòdul és pur Baixa: la unitat funciona, el conjunt no se sap Errors de lògica i casos límit difícils de reproduir per la interfície
D'integració Diverses peces juntes: un component amb els seus fills, el seu estat i els seus proveïdors Alta (desenes o centenars de ms) Mitjà Alta: és com s'usa de debò Cablejat trencat entre peces que funcionen per separat
D'extrem a extrem (e2e) L'aplicació sencera en un navegador real Baixa (segons per prova) Alt Màxima Fallats d'enrutament real, CSS que tapa un botó, sessió, xarxa real, diverses pantalles encadenades

Algunes precisions que solen faltar:

  • Les proves estàtiques són proves. ESLint amb eslint-plugin-react-hooks detecta avui una família sencera de fallades que el 2018 requerien proves: un useEffect amb dependències incompletes, un hook dins d'un if. Configurar-lo bé és la inversió més rendible del projecte, i és requisit previ, no alternativa.
  • La frontera entre unitària i integració és difusa, i no importa. Quan es prova TargetaBicicleta, que renderitza EtiquetaEstat a dins, és unitària o d'integració? És d'integració, i és el correcte. Discutir l'etiqueta és temps perdut; el que importa és el criteri de l'apartat següent.
  • La confiança no és proporcional al nombre de proves, sinó al seu tipus. Mil proves unitàries de funcions auxiliars no garanteixen que l'aplicació arrenqui. Una prova e2e que reserva una bicicleta sí.

  1. La piràmide de proves enfront del trofeu de proves

Durant dues dècades, el model dominant va ser la piràmide de proves (Mike Cohn, 2009): moltes unitàries a la base, algunes d'integració al mig, molt poques d'extrem a extrem al cim. La justificació era econòmica: les unitàries eren barates i ràpides; les d'integració, lentes i fràgils.

flowchart TB
    subgraph PIRAMIDE["Piramide classica"]
        direction TB
        P3["E2E · molt poques"]
        P2["Integracio · algunes"]
        P1["Unitaries · moltissimes"]
        P3 --- P2 --- P1
    end
    subgraph TROFEU["Trofeu de proves (front-end modern)"]
        direction TB
        T4["E2E · poques, nomes fluxos critics"]
        T3["Integracio · LA MAJORIA"]
        T2["Unitaries · les justes: logica pura i casos limit"]
        T1["Estatica · linter i tipus, gratis i continua"]
        T4 --- T3 --- T2 --- T1
    end

Kent C. Dodds va proposar el trofeu de proves per al front-end actual, i el canvi de forma respon a tres fets concrets:

  1. Les proves d'integració han deixat de ser cares. jsdom munta un DOM complet en memòria en mil·lisegons, i Testing Library permet interactuar-hi com ho faria una persona. El que el 2010 requeria aixecar un navegador avui costa 50 ms.
  2. L'anàlisi estàtica ha absorbit bona part del que provaven les unitàries. Un tipus mal passat o un hook mal cridat ja no necessiten prova: el linter i el compilador de tipus els atrapen abans.
  3. En una interfície, gairebé totes les fallades reals són al cablejat, no a les unitats. El component funciona, el reductor funciona, el hook funciona… i la pantalla és buida perquè el selector retorna undefined. Les proves unitàries de cada peça passen les tres. La prova d'integració falla, que és el que es vol.

D'aquí surt la regla operativa d'aquest mòdul, i la que s'aplicarà a CicloUrbano:

El pes cau en les proves d'integració de components. Les unitàries es reserven per a la lògica pura amb casos límit —validacions, reductors, selectors, utilitats— i les d'extrem a extrem per a tres o quatre fluxos crítics de negoci.

  1. El principi rector del mòdul: comportament, no implementació

Si d'aquest mòdul només t'enduus una idea, que sigui aquesta:

Prova el que l'usuari veu i fa, no com ho has escrit per dins.

La manera pràctica de saber si una prova respecta el principi és la prova de la refactorització: si pots reescriure l'interior del component sense canviar res del que l'usuari percep, i la prova falla, la prova està malament.

Mira-ho amb dues versions de la mateixa comprovació sobre SelectorTipus. Aquí només interessa el contrast; la mecànica de render i screen és matèria de 09-03.

// ❌ MALAMENT: comprova la implementació
test('actualitza l\'estat tipusTriat en prémer Elèctrica', () => {
  const component = muntarIEspiarEstat(<SelectorTipus />);
  component.prem('Elèctrica');
  // S'afirma sobre el NOM d'una variable interna
  expect(component.estat.tipusTriat).toBe('electrica');
});
// ✅ BÉ: comprova el comportament observable
test('marca el filtre premut com a actiu i avisa del tipus triat', async () => {
  const usuari = userEvent.setup();
  const alCanviarTipus = vi.fn();
  render(<SelectorTipus tipus="todos" alCanviarTipus={alCanviarTipus} />);

  await usuari.click(screen.getByRole('button', { name: 'Elèctrica' }));

  expect(alCanviarTipus).toHaveBeenCalledWith('electrica');
});

Ara aplica un canvi perfectament legítim: renombrar tipusTriat a tipusActiu, o substituir l'useState per un useReducer, o —com va passar de debò al mòdul 7— pujar aquest estat a Redux i passar el tipus per props.

Canvi a la implementació La prova «MALAMENT» La prova «BÉ»
Renombrar tipusTriattipusActiu Falla (fals positiu) Passa
useStateuseReducer Falla Passa
Estat pujat a Redux / a la URL Falla Passa
El botó deixa d'avisar el pare Passa (fallada real no detectada!) Falla

Aquesta última fila és la més eloqüent: la prova acoblada a la implementació no només es trenca quan no ha de fer-ho, sinó que deixa passar la fallada que sí que importa. Falla quan no toca i calla quan toca. És pitjor que no tenir prova, perquè a més consumeix temps de manteniment.

La llista de coses que no es proven mai, i que es repetirà a 09-03 amb exemples:

  • L'estat intern d'un component i el nom de les seves variables.
  • Els noms de les classes de CSS (estils.actiu és un hash generat per CSS Modules).
  • Quantes vegades s'ha renderitzat un component (per a això hi ha el Profiler de 08-05, que és una eina de diagnòstic, no de verificació).
  • Que s'hagi cridat un hook concret o un mètode concret d'una biblioteca.
  • Mètodes privats i funcions no exportades: si mereixen prova, és que mereixen ser exportades.

  1. Què val la pena provar a CicloUrbano i què no

Provar-ho tot és impossible i provar a l'atzar és inútil. El criteri que funciona combina dos eixos: quant fa mal si es trenca i quant costa la prova. Aplicat al codi que ja existeix al projecte:

Tipus de codi de CicloUrbano Exemples concrets Es prova? Amb quin tipus de prova Lliçó
Utilitats pures validarReserva, disponibilitat.js, classes Sí, a fons Unitària, amb tots els casos límit 09-02
Reductors i selectors sliceReserves, sliceCataleg, createSelector Sí, a fons Unitària: són funcions pures, no necessiten React 09-02
Components de presentació EtiquetaEstat, TargetaBicicleta, Panell Sí, el just Integració lleugera: què pinta i què avisa 09-03
Formularis amb regles FormulariReserva Sí, prioritari Integració: omplir, enviar, veure errors 09-03
Hooks personalitzats useAlternar, useDebounce, useMagatzemLocal Sí, si tenen lògica pròpia Unitària amb renderHook 09-04
Pàgines amb dades del servidor PaginaCataleg, PaginaDetallEstacio Sí, els tres estats Integració amb la xarxa simulada (MSW) 09-04
Mutacions useCrearReserva i la seva invalidació Integració amb MSW 09-04
Fluxos complets de negoci Identificar-se · reservar · cancel·lar Sí, només aquests tres Extrem a extrem amb Cypress 09-05
Estils i maquetació CSS Modules, variables :root, tema fosc No amb proves de codi Revisió visual; com a molt, regressió visual
Codi de tercers React Router, TanStack Query, Redux Toolkit No Ja estan provats pels seus autors
Embolcalls sense lògica Panell si només pinta children dins d'una <section> No val la pena
Constants i dades fictícies dades/domini.js No Provar una constant és tautològic

Dos criteris més per desempatar quan dubtis:

  • Ha fallat mai? Cada fallada real que arriba a producció mereix una prova que la reprodueixi abans d'arreglar-la. Aquesta prova és la que garanteix que no torni.
  • És al camí dels diners? A CicloUrbano, crear una reserva és el camí dels diners. Canviar el tema a fosc no ho és. El pressupost de proves es reparteix en conseqüència.

  1. Falsos amics: cobertura, proves fràgils i proves inestables

8.1. La cobertura com a mètrica

La cobertura de codi (code coverage) mesura quin percentatge del codi s'ha executat durant les proves, normalment desglossat en quatre dimensions:

Mètrica Què mesura
Línies (lines) Percentatge de línies executades
Sentències (statements) El mateix, però per sentència; difereix quan n'hi ha diverses en una línia
Branques (branches) Percentatge de camins d'if, ?:, &&, switch recorreguts. La més informativa
Funcions (functions) Percentatge de funcions cridades almenys una vegada

Per al que serveix de debò: per trobar el que no has provat. Obres l'informe, veus que la branca de l'error 500 de PaginaCataleg és en vermell, i decideixes si mereix una prova. Aquest ús és excel·lent.

Per al que no serveix: com a objectiu. Aquesta prova dona un 100 % de cobertura de validarReserva i no comprova absolutament res:

// 100 % de cobertura, zero valor
test('validarReserva no explota', () => {
  validarReserva({ bicicletaId: 'bici-001', dataInici: '2030-01-01T10:00', hores: 2, condicions: true }, []);
});

Executa la funció, recorre línies… i no té ni una sola asserció. La cobertura mesura execució, no verificació. Per això el 100 % no és l'objectiu: perseguir-lo empeny a escriure proves de farciment per a embolcalls trivials i a provar codi que no ho mereix, i el temps surt del pressupost de les proves que sí que importen. Un rang raonable en un projecte sa és entre el 70 % i el 85 %, amb una condició: que aquest percentatge inclogui la lògica de negoci. Un 90 % que deixa validarReserva sense cobrir val menys que un 60 % que la cobreix sencera.

8.2. Proves fràgils

Una prova fràgil (brittle) és la que falla davant canvis que no trenquen res. És exactament la de l'apartat 6, i les seves causes habituals són sempre les mateixes:

  • Assercions sobre detalls interns (estat, classes CSS, estructura exacta del DOM).
  • Selectors acoblats a la maquetació: «el tercer <div> dins de la segona <section>».
  • Textos complets i literals amb puntuació inclosa, que es trenquen en corregir un accent.
  • Dependència de l'ordre entre proves: la prova B només passa si abans s'ha executat l'A.

El símptoma reconeixible: a cada pull request cal «arreglar les proves». Quan això passa, l'equip deixa de creure's les fallades, i una prova en què ningú creu ja no protegeix res.

8.3. Proves inestables (flaky)

Una prova inestable és la que passa o falla amb el mateix codi, sense canviar res. És el pitjor dels mals, perquè destrueix l'única propietat que fa útil una suite: que una fallada signifiqui alguna cosa.

Causa habitual Com es manifesta Solució correcta
Esperar amb un temps fix (setTimeout(500)) Falla en una màquina lenta o en integració contínua Esperar que aparegui el que s'espera, amb findBy* / waitFor (09-04)
Estat compartit entre proves Falla només en executar la suite sencera, no en solitari Aïllar: memòria cau nova, magatzem nou, localStorage net a cada prova
Dependència del rellotge real Falla a mitjanit o en canviar l'hora Temporitzadors falsos i dates fixes (09-02)
Dependència de l'ordre d'execució Falla en executar en paral·lel Cada prova construeix el seu escenari complet
Xarxa real en una prova Falla quan l'API va lenta o és caiguda Interceptar la xarxa amb MSW (09-04)

I la regla més important sobre elles: una prova inestable no es reintenta, s'arregla o s'esborra. Marcar-la com «reintentar tres vegades» amaga una fallada real d'aïllament que, tard o d'hora, serà una fallada de l'aplicació.

  1. Com s'anomena una prova

El nom d'una prova és l'única cosa que es veu quan falla en un registre d'integració contínua a les tres de la matinada. Ha de bastar per entendre què s'ha trencat, sense obrir el fitxer.

La fórmula que millor funciona descriu el comportament esperat, no el mecanisme:

<subjecte> + què fa + en quina circumstància

❌ Nom pobre ✅ Nom útil
test('funciona') test('retorna un objecte buit quan totes les dades són vàlides')
test('validarReserva 2') test('rebutja una reserva de 25 hores perquè supera el màxim')
test('render') test('mostra el botó Reservar deshabilitat si la bicicleta és en manteniment')
test('setState') test('avisa el pare amb el tipus triat en prémer un filtre')

Convencions del projecte, perquè les cinc lliçons siguin coherents:

  • Els noms van en català, com tots els textos del projecte; describe, test i expect no es tradueixen, són API.
  • Els describe agrupen per subjecte: el nom del mòdul o del component, tal qual (describe('validarReserva', …), describe('TargetaBicicleta', …)).
  • Res de «hauria de»: test('rebutja…') en present d'indicatiu. És més curt i es llegeix millor a l'informe.
  • Un describe imbricat quan comparteixen circumstància: describe('quan la bicicleta és en manteniment', …).

  1. Posada en marxa de l'entorn: Vitest, jsdom i Testing Library

CicloUrbano es construeix amb Vite, i l'executor de proves natural per a un projecte Vite és Vitest: comparteix la mateixa configuració, el mateix sistema de resolució de mòduls i les mateixes transformacions, així que no hi ha una segona cadena de compilació a mantenir. I —això importa per a la lliçó següent— implementa la mateixa API que Jest, l'estàndard de l'ecosistema.

10.1. Instal·lació

npm install -D vitest jsdom @testing-library/react @testing-library/jest-dom @testing-library/user-event
Paquet Per a què
vitest L'executor: troba els fitxers de prova, els executa i dona l'informe
jsdom Implementació del DOM en JavaScript: dona un document i un window fora del navegador
@testing-library/react render, screen i les consultes per provar components com els usa una persona
@testing-library/jest-dom Matchers específics del DOM: toBeInTheDocument, toBeDisabled, toHaveValue
@testing-library/user-event Simula interaccions reals: clic, escriptura, tabulació

Tot va a devDependencies (-D): no forma part del paquet que es desplega.

10.2. Configuració a vite.config.js

// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],

  test: {
    // 1) Entorn: un DOM en memòria, perquè provem components
    environment: 'jsdom',

    // 2) describe / test / expect disponibles sense importar-los, com a Jest
    globals: true,

    // 3) Fitxer que s'executa UNA VEGADA abans de cada fitxer de proves
    setupFiles: './src/proves/configuracio.js',

    // 4) Què s'inclou a l'informe de cobertura
    coverage: {
      reporter: ['text', 'html'],
      include: ['src/**/*.{js,jsx}'],
      exclude: ['src/proves/**', 'src/main.jsx', 'src/**/*.test.{js,jsx}']
    }
  }
});

Apartat per apartat:

  1. environment: 'jsdom' és el que permet que render(<TargetaBicicleta />) tingui un document on pintar. Sense això, l'entorn per defecte és node i qualsevol accés al DOM llança document is not defined. Per provar només funcions pures, node és més ràpid; es pot fixar per fitxer amb un comentari // @vitest-environment node.
  2. globals: true fa que describe, test, expect, beforeEach i vi estiguin disponibles sense importar-los. És el que dona compatibilitat de codi amb Jest i el que permet que @testing-library/jest-dom s'hi enganxi. Sense aquesta opció cal importar tot de vitest a cada fitxer.
  3. setupFiles apunta al fitxer de preparació comú, que és l'apartat següent. Allà es registren els matchers i, a 09-04, s'arrencarà el servidor simulat de MSW.
  4. coverage exclou el que no té sentit mesurar: les pròpies utilitats de prova, el punt d'entrada main.jsx (que només crida createRoot) i els fitxers .test.

10.3. El fitxer de preparació: src/proves/configuracio.js

// src/proves/configuracio.js
// S'executa abans de CADA fitxer de proves (setupFiles a vite.config.js)

// 1) Registra els matchers de DOM: toBeInTheDocument, toBeDisabled, toHaveValue…
import '@testing-library/jest-dom/vitest';

// 2) Comoditats globals de l'entorn de proves de CicloUrbano
import { afterEach } from 'vitest';
import { cleanup } from '@testing-library/react';

// Testing Library neteja sola si `globals: true`, però deixar-ho explícit
// documenta la intenció i protegeix davant un canvi de configuració.
afterEach(() => {
  cleanup();
  localStorage.clear();   // aïlla les proves d'useMagatzemLocal
});

La importació és @testing-library/jest-dom/vitest, amb el sufix: aquesta variant crida expect.extend de l'expect de Vitest. Si importes @testing-library/jest-dom a seques a Vitest, els matchers poden no registrar-se i toBeInTheDocument apareixerà com «no és una funció».

localStorage.clear() a l'afterEach no és decoració: useMagatzemLocal i ProveidorTema hi escriuen, i sense neteja una prova heretaria el tema fosc que va deixar l'anterior. És prevenció directa del punt 8.3.

10.4. Estructura de fitxers del mòdul

Els fitxers de prova es col·loquen al costat del codi que proven, i les utilitats compartides en una carpeta pròpia:

src/
├── components/
│   ├── TargetaBicicleta.jsx
│   ├── TargetaBicicleta.test.jsx       ← al costat del component
│   ├── FormulariReserva.jsx
│   └── FormulariReserva.test.jsx
├── utilitats/
│   ├── validarReserva.js
│   └── validarReserva.test.js
├── hooks/
│   ├── useDebounce.js
│   └── useDebounce.test.js
└── proves/                             ← utilitats compartides, sense proves pròpies
    ├── configuracio.js                  (10.3, i MSW a 09-04)
    ├── utilitats.jsx                    (el `renderitzar` amb proveïdors, 09-03)
    ├── gestors.js                       (gestors de MSW, 09-04)
    └── servidor.js                      (setupServer de MSW, 09-04)

Col·locar la prova al costat del codi, i no en un __tests__ llunyà, té tres avantatges mesurables: es veu d'una ullada què està provat i què no, la ruta d'importació és './TargetaBicicleta.jsx' en lloc de '../../components/TargetaBicicleta.jsx', i en moure o esborrar un component la prova viatja o desapareix amb ell.

10.5. Els scripts de package.json

{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "api": "json-server --watch db.json --port 3001",
    "test": "vitest",
    "test:executar": "vitest run",
    "test:ui": "vitest --ui",
    "cobertura": "vitest run --coverage"
  }
}
Script Què fa Quan es fa servir
npm test Mode vigilància: es queda escoltant i reexecuta només el que s'ha vist afectat en desar Mentre programes. És el mode per defecte de Vitest
npm run test:executar Una passada i surt amb codi 0 o 1 Integració contínua i ganxos de pre-commit
npm run test:ui Interfície web amb l'arbre de proves, el DOM i els temps Depurar una fallada que no s'entén llegint la consola
npm run cobertura Genera l'informe a la consola i a coverage/index.html De tant en tant, per buscar forats (apartat 8.1)

test:ui requereix npm install -D @vitest/ui, i la cobertura requereix npm install -D @vitest/coverage-v8. Vitest ho demana per consola la primera vegada que executes l'script.

  1. La primera prova: verificar que l'entorn respira

Abans d'escriure una sola prova de l'aplicació, convé confirmar que la canonada funciona. Aquesta prova no comprova CicloUrbano: comprova Vitest, jsdom i els matchers.

// src/proves/entorn.test.js
import { describe, test, expect } from 'vitest';

describe('entorn de proves', () => {
  test('l\'executor de proves funciona', () => {
    expect(2 + 2).toBe(4);
  });

  test('jsdom proporciona un DOM en memòria', () => {
    // Si `environment` no fos 'jsdom', aquesta línia llançaria "document is not defined"
    document.body.innerHTML = '<h1>CicloUrbano</h1>';

    expect(document.querySelector('h1')).toBeInTheDocument();
    expect(document.querySelector('h1')).toHaveTextContent('CicloUrbano');
  });
});

Què verifica cadascuna:

  • La primera comprova que Vitest troba el fitxer, l'executa i avalua una asserció. Si aquesta falla, el problema és a la configuració, no al teu codi.
  • La segona comprova dues coses alhora: que environment: 'jsdom' és actiu, perquè existeix document; i que els matchers de jest-dom s'han registrat, perquè toBeInTheDocument i toHaveTextContent no són de Vitest, vénen del fitxer de preparació.
$ npm test

 ✓ src/proves/entorn.test.js (2)
   ✓ entorn de proves (2)
     ✓ l'executor de proves funciona
     ✓ jsdom proporciona un DOM en memòria

 Test Files  1 passed (1)
      Tests  2 passed (2)
   Start at  09:14:32
   Duration  412ms

Amb aquesta sortida en pantalla, l'entorn és a punt. Aquesta prova es pot esborrar després —no aporta res a llarg termini—, però és un bon primer pas, perquè separa «la meva prova està malament» de «la meva configuració està malament», que són dos problemes molt diferents i es depuren de maneres molt diferents.

  1. L'estratègia del Mòdul 9

Aquest és el pla, i respon punt per punt al buit que va deixar el mòdul 8:

Lliçó Què afegeix Sobre quin codi de CicloUrbano
09-01 (aquesta) Criteri, tipus de prova, entorn funcionant La configuració
09-02 L'executor, les assercions i els dobles de prova; funcions pures, sense React validarReserva, sliceReserves i els seus selectors, monitoritzacio.js
09-03 Components amb Testing Library: consultes, interacció, proveïdors EtiquetaEstat, TargetaBicicleta, SelectorTipus, FormulariReserva
09-04 Asincronia, temporitzadors, xarxa simulada amb MSW i hooks personalitzats PaginaCataleg, useCrearReserva, useDebounce, useMagatzemLocal, LimitError
09-05 Extrem a extrem en un navegador real amb Cypress, i integració contínua Els tres fluxos crítics i el flux de treball complet

I al mòdul 11, la lliçó 11-04 aplicarà tot això al projecte final. Aquí s'aprèn la tècnica; allà s'executa sobre una aplicació completa.

Errors Comuns i Consells

  • Escriure proves per pujar la cobertura. És l'error que més temps consumeix i menys aporta. La cobertura és un mapa per trobar forats, no una nota. Un 72 % amb la lògica de negoci coberta val més que un 95 % ple de proves d'embolcalls.
  • Provar l'estat intern «perquè és més fàcil». Sol ser-ho, i és exactament la raó per la qual cal resistir-s'hi: una prova fàcil que es trenca a cada refactorització té cost negatiu. Si provar el comportament costa molt, gairebé sempre vol dir que el component fa massa coses; el senyal és útil.
  • Començar per les proves d'extrem a extrem. Són les que donen més confiança i les més cares. Començar per aquí produeix una suite lenta i inestable que l'equip acaba desactivant. Es comença per allò pur i per la integració de components; e2e al final i només per als fluxos crítics.
  • Confondre «no té proves» amb «cal provar-ho tot ja». En un projecte existent, l'estratègia que funciona és: prova cada fallada abans d'arreglar-la, prova cada funcionalitat nova, i prova les zones que toques. La cobertura creix per on es mou el projecte, que és on fa falta.
  • Deixar proves comentades o amb test.skip permanent. Una prova desactivada és una mentida emmagatzemada: sembla que hi ha xarxa on no n'hi ha. S'arregla o s'esborra; l'historial de Git guarda el que es va esborrar.
  • No executar les proves en integració contínua. Una suite que només s'executa quan algú se'n recorda no protegeix res. A 09-05 veuràs el flux de treball mínim de GitHub Actions que les llança a cada canvi.
  • Consell: instal·la primer, escriu després. Deixa l'entorn de l'apartat 10 funcionant amb la prova trivial de l'11 abans d'intentar provar res real. Depurar una configuració i una prova alhora multiplica el temps per tres.
  • Consell: quan una prova falli, llegeix-la com si fos un informe de fallada. Si el nom i el missatge no et diuen què s'ha trencat, el problema és la prova. Millora-la en aquell moment, quan tens el context al cap.

Exercicis

Exercici 1. Classifica cadascun d'aquests comportaments de CicloUrbano segons el tipus de prova que li correspon (estàtica, unitària, d'integració o d'extrem a extrem) i justifica per què no li correspon el nivell immediatament inferior:

a) validarReserva rebutja una reserva de 25 hores. b) FormulariReserva mostra el missatge «La reserva màxima és de 24 hores» al costat del camp de durada quan s'escriu 25 i s'envia. c) Ana Ribera entra a /acceso, reserva bici-001 i veu la reserva a /reservas. d) useEffect a CercadorBicicletes declara alBuscar a les seves dependències. e) seleccionarBicicletesVisibles retorna les bicicletes urbanes ordenades per preu quan l'estat té tipus: 'urbana' i ordre: 'preu'.

Exercici 2. Un company presenta aquesta prova de TargetaBicicleta i afirma que dona confiança perquè cobreix el 100 % del component. Identifica quatre problemes diferents i reescriu l'enunciat de les proves que la substituirien (només els noms, no la implementació).

test('TargetaBicicleta', () => {
  const { container } = render(
    <TargetaBicicleta bicicleta={{ id: 'bici-001', model: 'Urbana Clàssica', estat: 'disponible', preuHora: 2.5 }} />
  );
  expect(container.querySelector('.targeta_a3f9x')).toBeTruthy();
  expect(container.querySelectorAll('button').length).toBe(2);
  expect(container.innerHTML).toContain('Urbana Clàssica');
});

Exercici 3. L'equip de CicloUrbano té una prova que falla aproximadament una de cada deu execucions en integració contínua i sempre passa en local. Comprova que, després de crear una reserva, apareix l'avís «Reserva creada». Un membre de l'equip proposa embolcallar-la en retry(3). Explica per què és mala idea, enumera tres causes possibles de la inestabilitat donades les eines del projecte, i descriu què comprovaries primer i per què.

Solucions

Solució 1.

Cas Tipus Per què no el nivell inferior
a Unitària És lògica pura sense DOM ni React. Baixar més només deixaria l'anàlisi estàtica, que no pot saber que el màxim són 24 hores: això és una regla de negoci, no un tipus
b Integració Una unitària de validarReserva confirma que l'error es genera, però no que el formulari el mostri ni que l'associï al camp correcte. La fallada típica aquí és de cablejat: l'error existeix i no es pinta perquè tocats no s'ha actualitzat
c Extrem a extrem Encadena tres pantalles, navegació real, sessió i persistència. Una prova d'integració de cada pàgina passaria encara que la redirecció després de l'accés portés a la ruta equivocada
d Estàtica eslint-plugin-react-hooks ho detecta en desar, gratis i sense executar res. Escriure una prova per a això seria llençar els diners
e Unitària Un selector és una funció pura de (estat) => resultat. Muntar components per verificar-lo afegeix cost, soroll al diagnòstic i cap confiança addicional

Solució 2. Els quatre problemes:

  1. El nom no descriu res. test('TargetaBicicleta') no diu quin comportament es verifica, així que la seva fallada en integració contínua no informa. I el describe hauria de dur aquest nom, no el test.
  2. Asserció sobre una classe de CSS Modules. .targeta_a3f9x és un identificador generat: canvia a cada compilació i amb cada versió de Vite. A més, que existeixi un div amb aquesta classe no és un comportament que l'usuari percebi.
  3. Comptar botons amb querySelectorAll. Es comprova l'estructura del DOM, no la funció. Si s'afegeix un tercer botó legítim, la prova falla sense que res estigui trencat; i si el botó «Reservar» deixa d'estar deshabilitat en manteniment —la fallada real que importa—, la prova continua passant.
  4. Diverses comprovacions inconnexes en una sola prova i sense screen. S'usen container.querySelector i innerHTML en lloc de les consultes accessibles, i en fallar només se sap que «TargetaBicicleta» falla, no quina de les tres coses.

Els enunciats que la substitueixen:

describe('TargetaBicicleta')
  test('mostra el model, l\'estació i el preu per hora')
  test('permet reservar una bicicleta disponible i avisa amb el seu identificador')
  test('deshabilita el botó Reservar quan la bicicleta és en manteniment')
  test('avisa amb l\'identificador en prémer «Veure fitxa»')

Solució 3. Per què retry(3) és mala idea: no arregla res, amaga el símptoma i, sobretot, desactiva el senyal. Si la prova falla una de cada deu vegades per una condició de cursa real, aquesta mateixa condició de cursa afecta els usuaris; tapar-la amb reintents converteix una fallada de producció detectada en una fallada de producció invisible. A més contamina el criteri de l'equip: així que s'accepta que «de vegades fallen», ningú torna a mirar una fallada vermella.

Tres causes possibles amb les eines del projecte:

  1. Espera per temps en lloc de per resultat. Si la prova usa un retard fix després d'enviar el formulari, a la màquina més lenta d'integració contínua la mutació encara no ha resolt quan es comprova l'avís. Es corregeix esperant l'avís amb findByText, que reintenta fins que apareix (09-04).
  2. Estat compartit entre proves. Un QueryClient reutilitzat entre fitxers deixa en memòria cau la llista de reserves d'una prova anterior; segons l'ordre d'execució, la llista ja conté la reserva i l'avís mai es dispara. Es corregeix creant un client nou per prova amb retry: false.
  3. La xarxa no està interceptada. Si la prova arriba a json-server de debò, depèn que el procés estigui aixecat, de la seva latència i que db.json no hagi estat modificat per una altra prova. Es corregeix amb MSW.

Què comprovaria primer: executar el fitxer en solitari i després dins de la suite completa. És el diagnòstic més barat i separa d'un cop les dues famílies de causes. Si passa en solitari i falla a la suite, el problema és d'aïllament (causes 2 i 3). Si falla en tots dos casos de forma intermitent, el problema és d'espera (causa 1). Sense aquesta bifurcació, qualsevol correcció és a cegues.

Conclusió

Aquesta lliçó ha posat el criteri abans que l'eina, que és l'ordre correcte: sense criteri, una bateria de proves es converteix en un llast que cal «arreglar» a cada pull request.

L'essencial que t'enduus. Una prova automatitzada no és més que una funció amb tres parts —preparar, actuar i comprovar— i aporta tres coses diferents: permet refactoritzar sense por (el buit exacte que van deixar les nou refactoritzacions del mòdul 8), serveix de documentació executable que no pot mentir sense fallar, i detecta regressions abans que les compti un usuari. Hi ha quatre nivells —estàtica, unitària, d'integració i d'extrem a extrem— i cadascun detecta fallades que els altres no veuen: el linter atrapa el hook mal declarat, la unitària el cas límit de 25 hores, la d'integració el cablejat trencat entre peces que funcionen per separat, i la e2e el CSS que tapa el botó de reservar. El repartiment de pes ja no és la piràmide clàssica sinó el trofeu: en front-end modern la majoria de les proves són d'integració, perquè jsdom les ha abaratit, perquè l'anàlisi estàtica ha absorbit bona part de les unitàries i perquè és allà on són les fallades de debò.

Per damunt de tot queda el principi rector del mòdul: es prova el comportament visible, no la implementació. La prova que afirma sobre tipusTriat falla en renombrar una variable i calla quan el component deixa d'avisar el pare —falla quan no toca i calla quan toca—; la que afirma que alCanviarTipus rep 'electrica' sobreviu a useState, a useReducer i a pujar l'estat a Redux, i es trenca només si es trenca alguna cosa. D'aquí surt la llista del que no es prova mai: estat intern, classes de CSS Modules, nombre de renders i detalls de biblioteques de tercers. I el repartiment per a CicloUrbano: a fons les utilitats pures, els reductors i els selectors; amb integració els components i el formulari; amb MSW les pàgines amb dades; amb Cypress només tres fluxos crítics; i res per als estils, les constants i el codi de tercers.

Coneixes també els falsos amics. La cobertura és un mapa per trobar forats, no un objectiu: mesura execució, no verificació, i una prova sense una sola asserció pot donar el 100 %. Les proves fràgils fallen davant canvis innocus i acaben amb la credibilitat de la suite. Les inestables són pitjors, perquè destrueixen el significat del color vermell, i no es reintenten: s'arreglen o s'esborren. I saps anomenar una prova perquè la seva fallada s'entengui sola, amb la fórmula subjecte + què fa + en quina circumstància.

Finalment, l'entorn és muntat i verificat: Vitest amb environment: 'jsdom', globals: true i setupFiles: './src/proves/configuracio.js'; els matchers de @testing-library/jest-dom/vitest registrats; localStorage net entre proves; els fitxers .test.jsx al costat del codi i les utilitats compartides a src/proves/; i els scripts npm test, npm run test:ui i npm run cobertura. La prova trivial d'entorn.test.js confirma en 400 ms que la canonada sencera funciona, que és el que separa «la meva prova està malament» de «la meva configuració està malament».

Amb el criteri fixat i l'entorn respirant, toca escriure proves de debò. La propera lliçó es queda deliberadament fora de React: l'executor per dins, l'anatomia d'un fitxer de proves amb describe i els seus ganxos, el catàleg d'assercions —començant per la diferència entre toBe i toEqual, que causa més confusió que cap altra—, els dobles de prova amb vi.fn() i vi.mock, i el control del temps amb temporitzadors falsos. Tot això aplicat a validarReserva i al reductor sliceReserves, tancant per fi el que es va prometre a 05-05 i 07-04: que un reductor pur es prova cridant-lo. I com que l'índex mana, es farà des del model de Jest, amb la taula d'equivalències que et permetrà endur-te el que has après a qualsevol projecte. La propera lliçó és Proves Unitàries amb Jest.

Curs de React

Mòdul 1: Introducció a React

Mòdul 2: Components de React

Mòdul 3: Treballar amb Esdeveniments

Mòdul 4: Conceptes Avançats de Components

Mòdul 5: Hooks de React

Mòdul 6: Enrutament a React

Mòdul 7: Gestió de l'Estat

Mòdul 8: Optimització del Rendiment

Mòdul 9: Proves a React

Mòdul 10: Temes Avançats

Mòdul 11: Projecte: Construir una Aplicació Completa

© Copyright 2026. Tots els drets reservats