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
- El cost real de refactoritzar sense proves
- Què és una prova automatitzada: preparar, actuar, comprovar
- Les tres coses que aporta una prova
- Tipus de prova: estàtica, unitària, d'integració i d'extrem a extrem
- La piràmide de proves enfront del trofeu de proves
- El principi rector del mòdul: comportament, no implementació
- Què val la pena provar a CicloUrbano i què no
- Falsos amics: cobertura, proves fràgils i proves inestables
- Com s'anomena una prova
- Posada en marxa de l'entorn: Vitest, jsdom i Testing Library
- La primera prova: verificar que l'entorn respira
- L'estratègia del Mòdul 9
- 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.
- 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.
- 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.
- 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-hooksdetecta avui una família sencera de fallades que el 2018 requerien proves: unuseEffectamb dependències incompletes, un hook dins d'unif. 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 renderitzaEtiquetaEstata 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í.
- 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:
- Les proves d'integració han deixat de ser cares.
jsdommunta 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. - 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.
- 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.
- 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 tipusTriat → tipusActiu |
Falla (fals positiu) | Passa |
useState → useReducer |
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.
- 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ó |
Sí | 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.
- 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ó.
- 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,testiexpectno es tradueixen, són API. - Els
describeagrupen 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
describeimbricat quan comparteixen circumstància:describe('quan la bicicleta és en manteniment', …).
- 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:
environment: 'jsdom'és el que permet querender(<TargetaBicicleta />)tingui undocumenton pintar. Sense això, l'entorn per defecte ésnodei qualsevol accés al DOM llançadocument 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.globals: truefa quedescribe,test,expect,beforeEachiviestiguin disponibles sense importar-los. És el que dona compatibilitat de codi amb Jest i el que permet que@testing-library/jest-doms'hi enganxi. Sense aquesta opció cal importar tot devitesta cada fitxer.setupFilesapunta 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.coverageexclou el que no té sentit mesurar: les pròpies utilitats de prova, el punt d'entradamain.jsx(que només cridacreateRoot) 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.
- 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è existeixdocument; i que els matchers dejest-doms'han registrat, perquètoBeInTheDocumentitoHaveTextContentno 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 412msAmb 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.
- 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.skippermanent. 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:
- 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 eldescribehauria de dur aquest nom, no eltest. - 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 undivamb aquesta classe no és un comportament que l'usuari percebi. - 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. - Diverses comprovacions inconnexes en una sola prova i sense
screen. S'usencontainer.querySelectoriinnerHTMLen 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:
- 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). - Estat compartit entre proves. Un
QueryClientreutilitzat 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 ambretry: false. - La xarxa no està interceptada. Si la prova arriba a
json-serverde debò, depèn que el procés estigui aixecat, de la seva latència i quedb.jsonno 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
- Què és React?
- Configuració de l'Entorn de Desenvolupament
- Hola Món amb React
- JSX: Extensió de Sintaxi de JavaScript
- Com Renderitza React: Virtual DOM i Reconciliació
Mòdul 2: Components de React
- Entendre els Components
- Components Funcionals vs de Classe
- Props: Passar Dades als Components
- State: Gestió de l'Estat del Component
- Estils en els Components: CSS, Mòduls i Utilitats
Mòdul 3: Treballar amb Esdeveniments
- Gestió d'Esdeveniments a React
- Renderitzat Condicional
- Llistes i Claus
- Formularis i Components Controlats
- Validació de Formularis i Components No Controlats
- Accessibilitat en Components Interactius
Mòdul 4: Conceptes Avançats de Components
- Elevar l'Estat
- Composició vs Herència
- Mètodes del Cicle de Vida de React
- Hooks: Introducció i Ús Bàsic
- Límits d'Error: Capturar Fallades a la Interfície
Mòdul 5: Hooks de React
- Hook useState
- Hook useEffect
- Hook useRef i Accés al DOM
- Hook useContext
- Hook useReducer
- Hooks Personalitzats
Mòdul 6: Enrutament a React
- Introducció a React Router
- Configuració de React Router
- Rutes Imbricades
- Navegació Programàtica
- Rutes Protegides i Control d'Accés
Mòdul 7: Gestió de l'Estat
- Introducció a la Gestió de l'Estat
- API de Context
- Redux: Introducció i Configuració
- Redux: Accions i Reductors
- Redux: Connectar-lo a React
- Estat del Servidor: Peticions, Memòria Cau i Sincronització
Mòdul 8: Optimització del Rendiment
- Tècniques d'Optimització del Rendiment a React
- Memoïtzació amb React.memo
- Hooks useMemo i useCallback
- Divisió de Codi i Càrrega Mandrosa
- Mesurar el Rendiment amb React DevTools Profiler
Mòdul 9: Proves a React
- Introducció a les Proves
- Proves Unitàries amb Jest
- Proves de Components amb React Testing Library
- Proves de Codi Asíncron i Simulació d'APIs
- Proves d'Extrem a Extrem amb Cypress
Mòdul 10: Temes Avançats
- Renderitzat al Servidor (SSR) amb Next.js
- Generació de Llocs Estàtics (SSG) amb Next.js
- Suspense i React Server Components
- TypeScript amb React
- React Native: Creació d'Aplicacions Mòbils
