El mòdul anterior acabava amb una afirmació i una pregunta. L'afirmació: la xarxa de Ribalta està protegida. La pregunta: com ho sabem? Sabem que un ciutadà no pot finalitzar el lloguer d'un altre perquè ho hem provat a mà amb curl una tarda de dimarts, i perquè llegint @PreAuthorize ens sembla que diu el que toca. Això no és coneixement: és confiança. I la confiança s'esvaeix així que algú reordena una regla d'authorizeHttpRequests, puja la versió d'una dependència o toca el SelectorTarifa per afegir-hi una tarifa nova.

CicloUrbana té avui una aplicació completa —REST, JPA sobre PostgreSQL, Flyway, seguretat amb JWT— i exactament zero proves: només el contextLoads buit que va generar Spring Initializr a la lliçó 01-04. Aquest mòdul ho corregeix. En aquesta primera lliçó encara no escriurem una suite: construirem el criteri. Què es prova i per què, quins tipus de prova existeixen i què detecta cadascun, com es reparteixen en una piràmide sana, què porta ja spring-boot-starter-test sense afegir res, on es col·loquen els fitxers, com s'anomenen les classes i els mètodes, quina és l'anatomia d'una prova ben escrita, com s'executen, com es mesura la cobertura amb JaCoCo i per què aquesta mètrica enganya si es converteix en objectiu. Al final tindrem escrita la primera prova real del projecte: petita, però verdadera.

Contingut

  1. Per què es prova
  2. Els tipus de prova
  3. La piràmide de proves i el con de gelat
  4. Què val la pena provar i què no
  5. spring-boot-starter-test: la caixa d'eines
  6. On viuen les proves: estructura i convencions
  7. Anatomia d'una prova: Preparar-Actuar-Comprovar
  8. Executar les proves
  9. Surefire i Failsafe: *Test davant d'*IT
  10. Cobertura amb JaCoCo
  11. TDD: vermell, verd, refactor
  12. Els dobles de prova
  13. Errors Comuns i Consells
  14. Exercicis

  1. Per què es prova

La resposta ingènua és «per trobar errors». És la menys important de les quatre raons reals.

Detectar regressions

Una regressió és una fallada introduïda en alguna cosa que abans funcionava. És la fallada més cara del cicle de vida d'un programa, perquè ningú no la busca: el canvi es va provar a mà, el canvi funciona, i el que s'ha trencat és tres capes més enllà.

A CicloUrbana hi ha desenes de regressions esperant el seu torn. Aquestes són reals i plausibles:

Canvi innocent Què es trenca en silenci
Afegir TarifaJubilat amb @Component sense nom() El SelectorTarifa la registra com a TarifaJubilat en lloc de jubilat; la tarifa no se selecciona mai
Reordenar dues regles a authorizeHttpRequests /api/v1/estacions/** passa a ser públic abans que la regla restrictiva l'abasti
Canviar @Transactional per @Transactional(readOnly = true) a finalitzar L'import es calcula però no es desa; ningú no ho nota fins a la facturació mensual
Reanomenar un camp d'EstacioResponse L'aplicació mòbil deixa de mostrar la capacitat; l'API respon 200
Afegir una columna a V7 sense tocar l'entitat ddl-auto: validate continua passant, però un INSERT falla amb NOT NULL en producció
Pujar la versió de Jackson Un Instant comença a serialitzar-se com a número; els clients es trenquen

Cap d'aquests canvis produeix un error de compilació. Tots els detecta una prova escrita una sola vegada i executada mil vegades.

Refactoritzar sense por

Refactoritzar és canviar l'estructura interna sense canviar el comportament observable. La definició conté un parany: per saber que el comportament no ha canviat cal poder comprovar-ho. Sense proves, «refactoritzar» és un eufemisme de «reescriure i resar», i el resultat pràctic és que ningú no toca el codi lleig: s'acumula, es volta i s'hereta.

Amb una suite decent, el mapejador de 03-05 que consulta repositoris —i que admetíem que estava mal dissenyat— es pot reescriure en una tarda. Sense ella, es queda allà per sempre.

Documentació viva

Un comentari menteix així que algú canvia el codi i no l'actualitza. Una prova que menteix falla. El nom d'un mètode de prova ben escrit és una frase del domini:

retorna404QuanEstacioNoExisteix
rebutjaLloguerQuanLaBateriaEstaPerSotaDelLlindar
elCiutadaNoPotFinalitzarElLloguerDunAltre
aplicaTarifaEstudiantAmbQuinzeMinutsGratuits

Llegits a l'informe d'una execució, aquests noms són l'especificació de CicloUrbana, i és una especificació que es compila i es verifica sola.

Pressió de disseny

Aquesta és la raó menys evident i la més valuosa. Un codi difícil de provar és un codi mal dissenyat, i la prova t'ho diu abans que cap revisor.

Si LloguerService cridés LocalDateTime.now() directament, provar «un lloguer de dues hores» exigiria esperar dues hores o manipular el rellotge del sistema. Que a la lliçó 02-01 injectéssim un Clock no va ser elegància gratuïta: va ser dissenyar per poder provar. El mateix val per a la injecció per constructor de 02-02 (permet construir l'objecte en una prova amb col·laboradors falsos), per a la interfície EstacioRepositori de 02-01 (permet substituir la implementació) i per a SeguretatLloguers de 05-05 (una regla de seguretat convertida en un bean normal, comprovable amb una prova unitària).

  1. Els tipus de prova

«Prova» no és una categoria única. Aquestes són les que ens interessen, ordenades de més ràpida i barata a més lenta i cara:

Tipus Què verifica Abast Velocitat típica Cost d'escriptura Cost de manteniment Què detecta que les anteriors no
Unitària Una classe o mètode aïllat Una unitat, col·laboradors falsos 1–10 ms Baix Baix Errors de lògica, càlcul i casos límit
De component / llesca Una capa amb part del framework Controlador o repositori + Spring 0,1–2 s Mitjà Mitjà Fallades d'anotacions, mapatges HTTP, JSON, consultes
D'integració Diverses capes cooperant de veritat Context complet + base de dades 1–10 s Mitjà-alt Mitjà Fallades de cablejat, transaccions, esquema, seguretat
D'extrem a extrem (E2E) Un cas d'ús complet per HTTP Aplicació desplegada 5–60 s Alt Alt Fallades de configuració d'entorn i desplegament
De contracte Que dos serveis continuen entenent-se La frontera entre dos sistemes 0,1–2 s Mitjà Mitjà Canvis incompatibles en una API publicada
De càrrega / rendiment Comportament sota concurrència i volum Sistema complet Minuts Alt Alt Degradació, fuites, límits del pool, N+1

Dos matisos que estalvien discussions estèrils:

  • La frontera entre unitària i integració és un continu, no una línia. El que importa no és l'etiqueta sinó dues propietats: si la prova és ràpida i si és determinista. Una prova que triga 40 ms i no toca xarxa ni disc es comporta com a unitària encara que instanciï tres classes reals.
  • Les de càrrega i les de contracte queden fora d'aquest mòdul. Les de rendiment apareixeran a 09-01, i les de contracte tenen sentit quan CicloUrbana es parteixi en diversos serveis (07-05 i 07-06). Aquí construïm les quatre primeres files.

  1. La piràmide de proves i el con de gelat

La piràmide de proves descriu la proporció sana entre tipus: moltes proves ràpides i barates a la base, molt poques lentes i fràgils al cim.

flowchart TB
    subgraph SA["Piràmide (sana)"]
        direction TB
        E1["E2E · poques · lentes"]
        I1["Integració i llesques · algunes"]
        U1["Unitàries · moltes · mil·lisegons"]
        E1 --- I1 --- U1
    end
    subgraph MAL["Con de gelat (antipatró)"]
        direction TB
        E2["E2E i manuals · moltíssimes"]
        I2["Integració · algunes"]
        U2["Unitàries · quatre"]
        E2 --- I2 --- U2
    end

La lògica de la forma és econòmica. Una prova unitària costa mil·lisegons, així que se'n poden tenir milers i executar-les cada cop que es desa el fitxer. Una E2E costa segons i falla de vegades sense motiu (la xarxa, un temps d'espera, una dada residual), així que cadascuna que hi afegeixes encareix la suite i erosiona la confiança que s'hi té.

El con de gelat és la piràmide invertida: gairebé tot es comprova arrencant l'aplicació sencera —o pitjor, a mà— i tot just existeixen proves unitàries. Els seus símptomes són inconfusibles:

  • La suite triga 25 minuts, així que ningú no l'executa en local.
  • Hi ha proves que fallen «de vegades» i l'equip les reintenta en lloc d'arreglar-les: són proves escamoses (flaky), i una de sola enverina la confiança en totes les altres.
  • Quan alguna cosa falla, el missatge és Expected 200 but was 500 i cal llegir 300 línies de log per saber què s'ha trencat. Una prova unitària et diu quin mètode i quin valor.

La proporció recomanada per a CicloUrbana, atesa la seva mida i la seva forma:

Nivell Proporció objectiu Què es prova aquí Pressupost de temps
Unitàries ~70 % CalculadoraTarifa i les seves tres implementacions, SelectorTarifa, regles d'EstacioService i LloguerService, SeguretatLloguers, mapejadors < 5 s en total
Llesques (@WebMvcTest, @DataJpaTest) ~20 % EstacioController i LloguerController, consultes del LloguerRepositori, serialització de DTOs < 30 s
Integració amb context i PostgreSQL real ~9 % Flux llogar-tornar complet, migracions Flyway, regles de seguretat d'extrem a extrem < 2 min
E2E sobre l'entorn desplegat ~1 % Un grapat de camins crítics: registre, login, llogar, tornar Fora del cicle de desenvolupament

No són percentatges que calgui mesurar amb un full de càlcul. Són un recordatori: si escriure una prova nova t'obliga sempre a aixecar el context d'Spring, el problema no és la prova, és el disseny de la classe.

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

Escriure proves costa temps i mantenir-les costa més. Gastar-lo bé exigeix dir que no.

Sí que val la pena provar:

  • La lògica de negoci amb branques: el càlcul de tarifes amb els seus minuts gratuïts, la regla dels ancoratges mínims, la comprovació del llindar de bateria, el recàrrec per superar la durada màxima.
  • Els casos límit i els límits exactes: 0 minuts, 1 minut, exactament 15 minuts amb la tarifa d'estudiant, exactament el llindar de bateria, l'estació amb un ancoratge lliure i amb cap.
  • Els camins d'error: què passa quan l'estació no existeix, quan la bicicleta està en manteniment, quan el lloguer ja està finalitzat.
  • Les regles de seguretat, sense excepció. Són les que fallen en silenci i les que surten més cares.
  • El contracte públic de l'API: els codis d'estat, la forma del JSON, els ProblemDetail de 03-06.
  • Tot error trobat en producció: abans d'arreglar-lo, una prova que el reprodueixi. És la regla que impedeix que la mateixa fallada torni dues vegades.

No val la pena provar:

Què Per què no
Getters, setters i record sense lògica No hi ha comportament a verificar; la prova només repeteix el codi
toString, equals autogenerats Tret de l'equals d'una entitat JPA, que sí que té regles pròpies (04-03) i mereix prova
Que Spring injecti un bean Estaries provant Spring, no CicloUrbana
Que @GetMapping mapegi una ruta trivial sense lògica Ho cobreix una sola prova de llesca del controlador, no una per mètode
Que Flyway apliqui migracions Amb matís: no provem Flyway, sinó que les nostres migracions deixen l'esquema que les entitats esperen (06-05)
Configuració trivial (server.port) Si falla, no arrenca; l'arrencada ja és la prova
Codi de tercers Si dubtes d'una biblioteca, la prova correcta és una prova de la teva integració amb ella, no d'ella

La pregunta que resol gairebé tots els casos dubtosos: «si això es trenca, me n'assabento abans que se n'assabenti un ciutadà de Ribalta?» Si la resposta és «només amb una prova», escriu-la.

  1. spring-boot-starter-test: la caixa d'eines

Spring Initializr ja va afegir aquesta dependència a 01-03, i és l'única que necessitem per a tot el mòdul tret de dos afegits concrets (seguretat a 06-04 i contenidors a 06-05):

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

El <scope>test</scope> és essencial: aquestes biblioteques s'usen en compilar i executar src/test/java, i no s'empaqueten al JAR de producció. Un assertThat no pot acabar a l'artefacte desplegat.

El que porta a dins:

Biblioteca Per a què serveix La farem servir?
JUnit 5 (Jupiter) El motor de proves: @Test, cicle de vida, parametrització Constantment (06-02)
Spring Test i Spring Boot Test @SpringBootTest, llesques, MockMvc, memòria cau de contextos 06-04 i 06-05
AssertJ Assercions fluides: assertThat(x).isEqualTo(y) És l'estil oficial del curs
Hamcrest Assercions amb matchers: assertThat(x, is(y)) Només dins de jsonPath(...) a MockMvc
Mockito 5 Dobles de prova: mock, when, verify 06-03
JSONassert Comparar dos documents JSON ignorant l'ordre Puntualment a 06-04
JsonPath Extreure valors d'un JSON amb expressions $.contingut[0].nom A cada prova de controlador
Awaitility Esperar que una condició asíncrona es compleixi, sense Thread.sleep A 07-03, amb @Async
XMLUnit Comparar documents XML No: CicloUrbana només parla JSON
JUnit Vintage Executar proves JUnit 4 antigues No: el projecte neix en JUnit 5

Per què no cal afegir dependències soltes. El BOM d'Spring Boot fixa versions compatibles entre si de totes elles. Si afegeixes org.mockito:mockito-core amb una versió pròpia, acabes amb dos Mockito al classpath i errors d'arrencada de l'extensió que costen mig matí de diagnosticar. La regla del projecte: si una cosa ja és al starter, no es declara; i si necessites canviar-ne la versió, es fa amb <mockito.version> a <properties>, que és la palanca que el BOM ofereix per a això.

Comprova què tens realment amb:

./mvnw dependency:tree -Dincludes=org.mockito,org.assertj,org.junit.jupiter

  1. On viuen les proves: estructura i convencions

Maven separa el codi de producció del de proves en dos arbres paral·lels:

ciclourbana/
├── src/main/java/com/ciclourbana/lloguers/TarifaEstandard.java
├── src/main/resources/application.yml
├── src/test/java/com/ciclourbana/lloguers/TarifaEstandardTest.java
└── src/test/resources/application-test.yml

Tres regles que se segueixen sense excepció:

  1. La prova viu al mateix paquet que la classe provada, encara que en un altre arbre. Així pot accedir a membres amb visibilitat de paquet sense obrir la classe de producció, i l'arbre de proves és un mirall navegable el de producció.
  2. src/test/resources va abans que src/main/resources al classpath de prova. Un application.yml col·locat allà no se suma al de producció: el substitueix del tot. És un error clàssic; la manera correcta de configurar les proves és un perfil (application-test.yml), que veurem a 06-04.
  3. Res de src/test/java acaba al JAR. Hi pots crear les classes de suport que vulguis.

Convencions de noms

Element Convenció Exemple a CicloUrbana
Classe de prova unitària o de llesca <ClasseProvada>Test TarifaEstandardTest, EstacioControllerTest
Classe de prova d'integració <Cas>IT LloguerFluxCompletIT
Classe de suport (dades, base comuna) Nom descriptiu, sense Test ni IT EstacionsDeProva, ProvaIntegracioBase
Mètode de prova Frase en català, descriptiva, sense test al davant calculaQuatreDeuPerTrentaMinuts

Sobre els noms de mètode, la política del curs mereix una justificació. testCalcular1() no diu res: quan falla a la integració contínua a les tres de la tarda, cal obrir el fitxer. rebutjaEstacioQuanLaCapacitatEsMenorQueVuit descriu la regla de negoci de Ribalta, en l'idioma del projecte, i l'informe de fallades es llegeix com una llista de requisits incomplerts. Tres formes vàlides, tria'n una i sigues consistent:

// 1. Frase directa (la que fa servir aquest curs)
void retorna404QuanEstacioNoExisteix()

// 2. Estructura donat-quan-llavors explícita
void donadaEstacioPlena_quanEsRetornaUnaBicicleta_llavorsLlancaEstacioPlenaException()

// 3. Frase curta + @DisplayName per a l'informe (es veu a 06-02)
@DisplayName("Retorna 404 quan l'estació no existeix")
void estacioInexistent()

I una nota que evita una fallada desconcertant: a JUnit 5 les classes i els mètodes de prova no cal que siguin public. La visibilitat de paquet és suficient i és la convenció actual. El que sí que continua prohibit és que siguin private o static.

  1. Anatomia d'una prova: Preparar-Actuar-Comprovar

Tota prova ben escrita té tres parts, separades visualment. El patró es coneix com Arrange-Act-Assert (AAA) o Given-When-Then; en aquest curs l'anomenarem Preparar-Actuar-Comprovar:

Fase Què fa Regla
Preparar Construeix l'estat i les dades d'entrada Només el que aquesta prova necessita
Actuar Executa una sola operació: la que s'està provant Una línia, gairebé sempre
Comprovar Verifica el resultat Un sol concepte verificat

La primera prova real de CicloUrbana. Provem TarifaEstandard de 02-02: 0,50 € de desbloqueig més 0,12 € per minut.

package com.ciclourbana.lloguers;

import org.junit.jupiter.api.Test;

import java.math.BigDecimal;
import java.time.Duration;

import static org.assertj.core.api.Assertions.assertThat;

class TarifaEstandardTest {

    @Test
    void cobraDesbloqueigMesDotzeCentimsPerMinut() {
        // Preparar
        TarifaEstandard tarifa = new TarifaEstandard();
        Duration durada = Duration.ofMinutes(30);

        // Actuar
        BigDecimal importTotal = tarifa.calcular(durada);

        // Comprovar: 0,50 + (0,12 × 30) = 4,10
        assertThat(importTotal).isEqualByComparingTo("4.10");
    }
}

Cada línia té una decisió al darrere:

  • class TarifaEstandardTest, sense public, al paquet com.ciclourbana.lloguers, mirall exacte de la classe provada.
  • new TarifaEstandard(): no hi ha Spring enlloc. TarifaEstandard és una classe Java normal que resulta estar anotada amb @Component; l'anotació no impedeix instanciar-la. Aquesta prova s'executa en menys d'un mil·lisegon, i aquesta és exactament la propietat que la fa útil.
  • import static ...Assertions.assertThat: l'import estàtic d'AssertJ, present a totes les proves del curs.
  • isEqualByComparingTo("4.10") i no isEqualTo(new BigDecimal("4.10")). L'equals de BigDecimal compara també l'escala, de manera que 4.1 i 4.10 no són iguals per a ell encara que valguin el mateix. Amb diners, sempre isEqualByComparingTo. És el primer dels paranys d'AssertJ i es detalla a 06-02.
  • El comentari del càlcul esperat: 4.10 no és un número màgic si al costat hi ha l'operació que el produeix. Qui llegeixi la prova d'aquí a un any sabrà si la fallada és al codi o a l'expectativa.

Executem-la:

./mvnw test -Dtest=TarifaEstandardTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS

CicloUrbana té la seva primera prova. És minúscula, i tanmateix ja afirma una cosa que abans només era al cap de qui va escriure la classe.

Vegem ara com falla, que és el que de veritat importa. Si algú canvia PER_MINUT a 0.15:

[ERROR] TarifaEstandardTest.cobraDesbloqueigMesDotzeCentimsPerMinut:19
expected: 4.10
 but was: 5.00

El missatge diu el nom de la regla incomplerta, el valor esperat i l'obtingut. La qualitat d'un missatge de fallada és una característica de la prova, no un accident, i és la raó principal per la qual aquest curs fa servir AssertJ.

  1. Executar les proves

# Totes les proves del projecte
./mvnw test

# Una classe concreta
./mvnw test -Dtest=TarifaEstandardTest

# Un mètode concret
./mvnw test -Dtest=TarifaEstandardTest#cobraDesbloqueigMesDotzeCentimsPerMinut

# Diverses classes, amb comodins
./mvnw test -Dtest='Tarifa*Test,SelectorTarifaTest'

# Compilar i empaquetar saltant-se les proves (per depurar l'empaquetatge)
./mvnw package -DskipTests

Un advertiment sobre aquesta darrera línia, perquè la diferència es pregunta a cada revisió de codi:

Opció Efecte
-DskipTests Compila les proves però no les executa
-Dmaven.test.skip=true Ni tan sols les compila: pot amagar que el codi de prova ja no compila

Fes servir sempre la primera. La segona deixa passar proves trencades sense avisar.

Des de l'IDE, el flux quotidià és diferent i millor: el triangle verd al costat del mètode executa aquesta prova en mil·lisegons, i la drecera de «repetir la darrera execució» (Ctrl+Shift+F10 a IntelliJ, Ctrl+F11 a Eclipse) és la que més s'utilitza mentre es programa. L'IDE executa JUnit directament, sense passar per Maven: és molt més ràpid, però no aplica la configuració de Surefire, de manera que una prova pot passar a l'IDE i fallar a ./mvnw test si depèn d'arguments de JVM o de perfils configurats al pom.xml. Abans de pujar res, ./mvnw verify.

  1. Surefire i Failsafe: *Test davant d'*IT

Maven té dos connectors de proves, i confondre'ls és la causa que moltes suites no executin mai les seves proves d'integració.

Surefire Failsafe
Fase del cicle test integration-test i verify
Què executa *Test, Test*, *Tests, *TestCase *IT, IT*, *ITCase
Si una prova falla Atura la construcció immediatament Registra la fallada i continua fins a post-integration-test
Quan s'executa Abans de package Després de package, sobre l'artefacte ja construït
Ús previst Proves unitàries i ràpides Proves que necessiten recursos externs

La diferència de comportament davant d'una fallada té una raó pràctica: si una prova d'integració aixeca un contenidor de PostgreSQL (06-05), Failsafe ha d'arribar sempre a la fase que l'apaga; per això no avorta a la primera fallada i deixa que verify sigui qui trenqui la construcció.

Failsafe no ve activat per defecte. S'afegeix així:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-failsafe-plugin</artifactId>
    <executions>
        <execution>
            <goals>
                <goal>integration-test</goal>
                <goal>verify</goal>
            </goals>
        </execution>
    </executions>
</plugin>

I a partir d'aquí, la convenció de CicloUrbana:

./mvnw test      # segons: unitàries i llesques. S'executa constantment.
./mvnw verify    # minuts: a més, les *IT amb PostgreSQL real. Abans de pujar i a CI.

Per què separar-les. Si les proves lentes estan barrejades amb les ràpides, la suite sencera triga minuts i deixa d'executar-se durant el desenvolupament; i quan una suite deixa d'executar-se, deixa de servir. Separar-les permet el cicle curt —desar, ./mvnw test, cinc segons— sense renunciar a la comprovació completa abans de pujar el canvi.

  1. Cobertura amb JaCoCo

La cobertura de codi mesura quin percentatge del codi s'executa durant la suite de proves. JaCoCo l'instrumenta en temps d'execució i genera un informe HTML. Es configura així:

<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.12</version>
    <executions>
        <execution>
            <id>preparar-agent</id>
            <goals>
                <goal>prepare-agent</goal>   <!-- enganxa l'agent abans de les proves -->
            </goals>
        </execution>
        <execution>
            <id>generar-informe</id>
            <phase>verify</phase>
            <goals>
                <goal>report</goal>          <!-- genera l'HTML després de les proves -->
            </goals>
        </execution>
    </executions>
    <configuration>
        <excludes>
            <!-- Classes sense lògica pròpia: incloure-les només dilueix el número -->
            <exclude>com/ciclourbana/**/dto/**</exclude>
            <exclude>com/ciclourbana/CicloUrbanaApplication.class</exclude>
        </excludes>
    </configuration>
</plugin>
./mvnw verify
# L'informe queda a: target/site/jacoco/index.html

L'informe acoloreix cada línia del codi font: verd si s'ha executat, vermell si no, groc si és una branca de la qual només s'ha recorregut una sortida (un if que sempre es va avaluar a cert). El groc és el color més informatiu: assenyala exactament els casos límit que falten per provar.

Dues mètriques conviuen a l'informe, i només una és interessant:

Mètrica Què mesura Utilitat
Cobertura de línies Línies executades / línies totals Baixa: una línia amb un && compta com a coberta havent provat un sol cas
Cobertura de branques Sortides de condició recorregudes / totals Alta: revela els if provats a mitges

L'advertiment, que és el més important d'aquest apartat

La cobertura mesura quin codi s'ha executat, no quin comportament s'ha verificat. Aquestes dues coses no s'assemblen gens, i aquí en tens la demostració:

@Test
void aquestTestDonaCoberturaTotalINoVerificaAbsolutamentRes() {
    new TarifaEstandard().calcular(Duration.ofMinutes(30));
    new TarifaEstudiant().calcular(Duration.ofMinutes(30));
    new TarifaJubilat().calcular(Duration.ofMinutes(30));
    // Ni un sol assert.
}

JaCoCo informarà del 100 % de cobertura de les tres tarifes. Si demà algú canvia el preu per minut de 0,12 € a 1,20 €, aquesta prova continuarà passant, i la ciutat de Ribalta facturarà deu vegades de més amb la construcció en verd.

D'aquí la llei de Goodhart aplicada al programari: quan una mesura es converteix en objectiu, deixa de ser una bona mesura. Si l'equip es marca «85 % de cobertura o falla la construcció», el que s'obté són proves sense assercions escrites la tarda abans del lliurament. La política de CicloUrbana:

  • La cobertura es mira, no es persegueix. El seu ús correcte és buscar el vermell: un if de negoci sencer sense cobrir és una pregunta legítima.
  • Un llindar mínim baix (40–50 %) que només impedeixi el retrocés grosser és defensable; un de alt és contraproduent.
  • Molt més valuós que el percentatge global és el percentatge del codi nou, que és el que els sistemes de revisió moderns mostren a cada canvi.

Si tot i així vols un llindar que trenqui la construcció, jacoco:check amb una regla:

<execution>
    <id>comprovar-cobertura</id>
    <phase>verify</phase>
    <goals><goal>check</goal></goals>
    <configuration>
        <rules>
            <rule>
                <element>BUNDLE</element>
                <limits>
                    <limit>
                        <counter>BRANCH</counter>
                        <value>COVEREDRATIO</value>
                        <minimum>0.50</minimum>
                    </limit>
                </limits>
            </rule>
        </rules>
    </configuration>
</execution>

  1. TDD: vermell, verd, refactor

El desenvolupament guiat per proves inverteix l'ordre habitual: primer la prova, després el codi. El seu cicle té tres passos i es repeteix en minuts, no en hores.

flowchart LR
    R["VERMELL<br/>Escriu una prova<br/>que falla"] --> V["VERD<br/>El codi més simple<br/>que la fa passar"]
    V --> F["REFACTOR<br/>Millora el disseny<br/>amb la prova en verd"]
    F --> R

Aplicat a una regla real de Ribalta —l'ajuntament exigeix que cap estació no tingui menys de 8 ancoratges, la capacitatMinima de XarxaProperties de 02-05:

Vermell. La prova s'escriu abans que existeixi el mètode:

@Test
void rebutjaEstacioQuanLaCapacitatEsMenorQueVuit() {
    EstacioService servei = new EstacioService(new EstacioRepositoriEnMemoria());

    assertThatThrownBy(() -> servei.validarCapacitat(6))
            .isInstanceOf(ReglaNegociException.class)
            .hasMessageContaining("8 ancoratges");
}

No compila: validarCapacitat no existeix. Això ja és el vermell, i és informatiu: en escriure la crida has dissenyat la signatura del mètode abans d'escriure'l.

Verd. El mínim que la fa passar:

public void validarCapacitat(int capacitat) {
    if (capacitat < 8) {
        throw new ReglaNegociException("CAPACITAT_INSUFICIENT",
                "Una estació de Ribalta necessita com a mínim 8 ancoratges");
    }
}

Refactor. Amb la prova en verd, el 8 literal se substitueix per xarxaProperties.capacitatMinima(). Si en fer-ho la prova continua passant, el refactor és correcte; si es trenca, ho has sabut en cinc segons.

Què aporta realment, més enllà de l'eslògan:

Avantatge En què es nota
Disseny des de fora Escrius la crida abans que la implementació, així que la signatura surt llegible
Només el codi necessari Costa escriure funcionalitat que ningú no ha demanat si primer cal justificar-la amb una prova
Cobertura com a conseqüència No es persegueix: apareix
Retroalimentació en segons El cicle dura minuts; l'error mai no és lluny

I una postura honesta: TDD no és obligatori i aquest curs no l'imposa. És especialment còmode en lògica algorítmica amb regles clares —tarifes, validacions, càlculs— i força incòmode quan estàs explorant una API que no coneixes. El que sí que és innegociable és que la prova existeixi abans de donar la tasca per acabada, s'escrigui abans o després.

  1. Els dobles de prova

Per provar LloguerService en aïllament cal donar-li un LloguerRepositori que no toqui PostgreSQL. Els objectes que substitueixen un col·laborador real s'anomenen genèricament dobles de prova, i no són tots iguals:

Doble Què fa Exemple a CicloUrbana Es verifica
Dummy Es passa per omplir un paràmetre, no s'usa mai Un Clock en un mètode que no el consulta Res
Stub Retorna respostes fixes preprogramades Un BicicletaRepositori que sempre retorna RB-0142 L'estat del resultat
Spy Objecte real que a més registra com se l'ha cridat Un SelectorTarifa real que anota quina tarifa s'ha demanat El comportament, parcialment
Mock Doble amb expectatives d'interacció definides Un ApplicationEventPublisher que ha de rebre LloguerIniciat El comportament
Fake Implementació real però simplificada EstacioRepositoriEnMemoria de 02-01, amb el seu ConcurrentHashMap L'estat

Dues observacions que orienten tot el mòdul 6:

  • A la pràctica quotidiana, «mock» s'usa per a tot, i Mockito hi contribueix: el seu mètode s'anomena mock(...) encara que l'habitual sigui fer-lo servir com a stub. La distinció conceptual continua important, perquè verificar l'estat produeix proves robustes i verificar interaccions produeix proves fràgils, acoblades a com està escrit el mètode per dins.
  • CicloUrbana ja té un fake escrit: EstacioRepositoriEnMemoria. En molts casos és preferible a cinc línies de when(...).thenReturn(...), i a 06-03 compararem totes dues opcions amb un criteri clar.

La lliçó 06-03 està dedicada per complet a Mockito. Aquí n'hi ha prou de reconèixer els noms.

Errors Comuns i Consells

Escriure proves sense assercions. El cas de l'apartat 10: s'executa el codi, no es comprova res, JaCoCo diu 100 %. Regla mecànica de revisió: tota prova té almenys un assertThat o un assertThatThrownBy; si no en té, no és una prova.

Confondre «no falla» amb «funciona». Cridar un mètode i comprovar que no llança excepció és l'asserció més feble possible. Comprova el valor retornat, l'estat resultant o la interacció concreta.

Proves dependents entre si. Si provaB necessita que provaA hagi inserit una dada, la suite és un castell de cartes: JUnit no garanteix l'ordre i canviar-ne una trenca l'altra. Cada prova prepara el que li cal i no deixa rastre. Si sents la temptació de fixar l'ordre amb @TestMethodOrder, gairebé sempre és el senyal d'un problema de disseny.

Posar un application.yml a src/test/resources. No es fusiona amb el de producció: el reemplaça, i les proves comencen a fallar per propietats que «hi són posades». Fes servir application-test.yml amb @ActiveProfiles("test") (06-04).

Fer servir @SpringBootTest per a tot. És l'error estructural que construeix el con de gelat. Aixecar el context per provar una fórmula de tarifa multiplica per mil el temps d'execució i no detecta ni una fallada més. El context s'aixeca quan el que proves és la integració.

Lògica dins de la prova. Un if, un for o un càlcul dins del bloc de comprovació introdueixen la possibilitat que la prova tingui el seu propi error, i llavors qui prova la prova? Els valors esperats s'escriuen literals; per a diversos casos existeixen les proves parametritzades de 06-02.

Consell: la prova s'escriu pensant en el dia que falli. Un nom que descrigui la regla, un missatge de fallada que s'entengui sense obrir el codi i una única raó per fallar. Quan una prova t'estalviï mitja hora de depuració a les onze de la nit, entendràs per què.

Consell: quan arribi una fallada de producció, la prova primer. Reprodueix l'error amb una prova que falla, arregla'l i observa com es posa en verd. Així se sap que s'ha arreglat el que es creia i es garanteix que no torni.

Consell: si provar és difícil, no forcis la prova: arregla el disseny. La necessitat de simular mètodes estàtics, d'instanciar sis col·laboradors o de manipular el rellotge del sistema són símptomes, no obstacles.

Exercicis

Exercici 1

Escriu TarifaEstudiantTest amb tres proves que cobreixin la regla dels 15 minuts gratuïts de 02-02: un lloguer de 10 minuts (gratis), un d'exactament 15 minuts (gratis: el límit és inclusiu) i un de 45 minuts. Aplica el patró Preparar-Actuar-Comprovar amb els comentaris de fase, fes servir isEqualByComparingTo i anomena els mètodes en català descrivint la regla. Justifica en un comentari per què el cas dels 15 minuts exactes és el més important dels tres.

Exercici 2

Configura JaCoCo al pom.xml de CicloUrbana segons l'apartat 10, executa ./mvnw verify amb les proves de les tarifes ja escrites i obre target/site/jacoco/index.html. Respon per escrit: quin percentatge de cobertura de branques té el paquet com.ciclourbana.lloguers? Quina classe del projecte té 0 %? I quin és l'if groc més preocupant que hi trobis? Després escriu una prova deliberadament inútil (sense assercions) sobre SelectorTarifa, torna a generar l'informe i anota quant ha pujat el percentatge.

Exercici 3

Classifica les vuit comprovacions pendents de CicloUrbana següents per tipus de prova (unitària, llesca, integració, E2E) i decideix per a cadascuna si val la pena escriure-la, justificant la decisió en una frase:

  1. Que TarifaJubilat.nom() retorna "jubilat".
  2. Que GET /api/v1/estacions/99 retorna un ProblemDetail amb estat 404.
  3. Que EstacioResponse és un record amb sis components.
  4. Que un ciutadà rep 403 en finalitzar el lloguer d'un altre.
  5. Que findByUsuariIdAndFiIsNull retorna només lloguers sense data de fi.
  6. Que la migració V4 crea l'índex sobre lloguers(inici).
  7. Que server.port val 8080.
  8. Que un token JWT caduca als quinze minuts.

Solucions

Solució 1

package com.ciclourbana.lloguers;

import org.junit.jupiter.api.Test;

import java.math.BigDecimal;
import java.time.Duration;

import static org.assertj.core.api.Assertions.assertThat;

class TarifaEstudiantTest {

    private final TarifaEstudiant tarifa = new TarifaEstudiant();

    @Test
    void noCobraResPerSotaDelsQuinzeMinutsGratuits() {
        // Preparar
        Duration durada = Duration.ofMinutes(10);
        // Actuar
        BigDecimal importTotal = tarifa.calcular(durada);
        // Comprovar: 10 < 15, tot dins del conveni universitari
        assertThat(importTotal).isEqualByComparingTo("0.00");
    }

    /*
     * El cas decisiu. El codi és Math.max(0, minuts - 15):
     * amb 15 minuts exactes el resultat és 0 i el lloguer és gratuït.
     * Si algú canviés la condició a "minuts > 15 ? ... : cobrar",
     * o convertís el límit en exclusiu, aquesta és l'ÚNICA prova de les
     * tres que es posaria en vermell. Els límits exactes són on viuen
     * els errors per un, i per això són els casos que mai no han de faltar.
     */
    @Test
    void ambQuinzeMinutsExactesElLloguerContinuaSentGratuit() {
        BigDecimal importTotal = tarifa.calcular(Duration.ofMinutes(15));

        assertThat(importTotal).isEqualByComparingTo("0.00");
    }

    @Test
    void cobraVuitCentimsPerCadaMinutQueExcedeixLaFranquicia() {
        // Preparar: 45 minuts -> 30 facturables
        Duration durada = Duration.ofMinutes(45);
        // Actuar
        BigDecimal importTotal = tarifa.calcular(durada);
        // Comprovar: 0,08 × (45 - 15) = 2,40
        assertThat(importTotal).isEqualByComparingTo("2.40");
    }
}

Comentari: el camp private final TarifaEstudiant tarifa com a estat compartit és acceptable aquí precisament perquè la classe no té estat mutable —la mateixa propietat que a 02-03 la feia segura com a singleton—. Si en tingués, cada prova hauria de crear la seva pròpia instància en un @BeforeEach. Cal notar també que les tres proves comproven amb isEqualByComparingTo("0.00") i no amb isZero(): isZero() funcionaria, però deixa de llegir-se com un import.

Solució 2

Només amb les proves de les tarifes escrites, l'informe mostra una cosa semblant a això:

Paquet Cobertura d'instruccions Cobertura de branques
com.ciclourbana.lloguers ~35 % ~40 % (només les tarifes)
com.ciclourbana.estacions 0 % 0 %
com.ciclourbana.seguretat 0 % 0 %
com.ciclourbana.comu 0 % 0 %
  • Amb 0 % hi ha gairebé totes, incloses les tres classes que més importen: LloguerService, GestorGlobalExcepcions i tot el paquet seguretat. Aquest és el valor real del primer informe: el mapa del que no està protegit.
  • El groc més preocupant sol ser TarifaEstandard.calcular: la línia Math.max(1, durada.toMinutes()) conté una branca que les proves de 30 i 45 minuts no recorren mai, la del lloguer de menys d'un minut. Aquí hi ha una regla de negoci amagada —«es cobra un minut com a mínim»— que ningú no ha verificat mai.
  • Després d'afegir la prova sense assercions sobre SelectorTarifa, la seva cobertura salta a prop del 100 % i el total del paquet puja uns quants punts. Ni un sol comportament nou està verificat. La conclusió de l'exercici és aquesta: el número va pujar, la seguretat no. La cobertura és útil llegida com a mapa del vermell, i enganyosa llegida com a puntuació.

Solució 3

# Tipus Val la pena? Justificació
1 Unitària Sí, encara que sigui trivial No es prova el return sinó el contracte amb SelectorTarifa: si algú esborra el nom(), la tarifa deixa de trobar-se i la fallada és silenciosa
2 Llesca (@WebMvcTest) Sí És contracte públic: codi d'estat i forma del ProblemDetail (06-04)
3 — No És la signatura de la classe: ho comprova el compilador
4 Llesca de seguretat o integració Sí, prioritària És la regla de 05-05, protegeix dades de tercers i falla en silenci (06-04)
5 Llesca (@DataJpaTest) Sí Una consulta derivada és codi generat a partir d'un nom: un canvi de nom l'altera sense avisar (06-04)
6 Integració amb PostgreSQL Sí És exactament el que H2 no pot validar; és el cas de 06-05
7 — No Si el port està malament, l'aplicació no arrenca: l'arrencada ja ho comprova
8 Unitària amb Clock fix Sí Regla de seguretat amb dependència temporal; el Clock injectat de ServeiJwt existeix just per a això (06-02)

El patró que emergeix de la taula: es prova el que pot fallar en silenci. El que trenca la compilació o impedeix l'arrencada ja té qui el vigili.

Conclusió

Aquest mòdul comença on acabava l'anterior: amb la sospita que CicloUrbana funciona i sense cap manera automàtica de demostrar-ho. Ara tens el criteri per construir-la. Saps que es prova per quatre raons —regressions, refactorització segura, documentació viva i pressió de disseny— i que la darrera explica decisions que arrosseguem des del mòdul 2: el Clock injectat, la injecció per constructor, la interfície EstacioRepositori i el bean SeguretatLloguers no eren elegància acadèmica, eren disseny per poder provar. Coneixes els sis tipus de prova amb el seu cost i el que detecta cadascun, la piràmide i el con de gelat, i la proporció concreta que busquem a CicloUrbana: setanta per cent d'unitàries que s'executen en segons, vint de llesques, nou d'integració amb base de dades real i un u per cent d'extrem a extrem.

Saps dir que no: no es proven els getters, ni el framework aliè, ni la configuració trivial, i sí tot allò que pot fallar en silenci, començant per les regles de seguretat i acabant per cada error trobat en producció. Tens inventariat el que spring-boot-starter-test ja porta —JUnit 5, Spring Test, AssertJ, Hamcrest, Mockito, JSONassert, JsonPath i Awaitility— i la raó per no afegir-hi ni una dependència solta al costat. Coneixes l'estructura mirall de src/test/java, el parany de col·locar un application.yml a src/test/resources, les convencions de noms del curs i el patró Preparar-Actuar-Comprovar, aplicat ja a TarifaEstandardTest: la primera prova real de CicloUrbana, amb el seu isEqualByComparingTo i el seu comentari del càlcul esperat. Saps executar-les amb ./mvnw test i els seus filtres, distingir Surefire de Failsafe i per què *Test i *IT viuen separades, configurar JaCoCo i —sobretot— llegir-lo com un mapa del vermell i mai com una puntuació, després de veure una prova sense assercions arribar al 100 %. I has vist el cicle vermell-verd-refactor aplicat a la regla dels vuit ancoratges, i la taula dels cinc dobles de prova.

Amb el criteri posat, toca la tècnica. La lliçó següent, Proves Unitàries amb JUnit, entra a fons a l'eina: l'arquitectura de JUnit 5, el cicle de vida complet de les seves anotacions, el catàleg d'assercions d'AssertJ per tipus de dada —amb el seu parany de BigDecimal i les assercions toves—, les proves parametritzades aplicades a les tres tarifes de Ribalta en una sola classe, l'organització amb @Nested i @DisplayName, el Clock.fixed que fa determinista el càlcul de la durada d'un lloguer i les bones pràctiques de dades de prova amb el patró Object Mother. Anem a omplir la base de la piràmide.

Curs de Spring Boot

Mòdul 1: Introducció a Spring Boot

Mòdul 2: Conceptes bàsics de Spring Boot

Mòdul 3: Construint serveis web RESTful

Mòdul 4: Accés a dades amb Spring Boot

Mòdul 5: Seguretat a Spring Boot

Mòdul 6: Proves a Spring Boot

Mòdul 7: Funcions avançades de Spring Boot

Mòdul 8: Desplegament d'aplicacions Spring Boot

Mòdul 9: Rendiment i monitoratge

Mòdul 10: Millors pràctiques i consells

© Copyright 2026. Tots els drets reservats