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
- Per què es prova
- Els tipus de prova
- La piràmide de proves i el con de gelat
- Què val la pena provar i què no
spring-boot-starter-test: la caixa d'eines- On viuen les proves: estructura i convencions
- Anatomia d'una prova: Preparar-Actuar-Comprovar
- Executar les proves
- Surefire i Failsafe:
*Testdavant d'*IT - Cobertura amb JaCoCo
- TDD: vermell, verd, refactor
- Els dobles de prova
- Errors Comuns i Consells
- Exercicis
- 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).
- 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.
- 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 500i 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.
- 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
ProblemDetailde 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.
spring-boot-starter-test: la caixa d'eines
spring-boot-starter-test: la caixa d'einesSpring 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:
- 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ó:
- 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ó.
src/test/resourcesva abans quesrc/main/resourcesal classpath de prova. Unapplication.ymlcol·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.- Res de
src/test/javaacaba 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.
- 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, sensepublic, al paquetcom.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'importestàtic d'AssertJ, present a totes les proves del curs.isEqualByComparingTo("4.10")i noisEqualTo(new BigDecimal("4.10")). L'equalsdeBigDecimalcompara també l'escala, de manera que4.1i4.10no són iguals per a ell encara que valguin el mateix. Amb diners, sempreisEqualByComparingTo. És el primer dels paranys d'AssertJ i es detalla a 06-02.- El comentari del càlcul esperat:
4.10no é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:
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:
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.
- 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 -DskipTestsUn 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.
- Surefire i Failsafe:
*Test davant d'*IT
*Test davant d'*ITMaven 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.
- 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>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
ifde 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>
- 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.
- 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 dewhen(...).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:
- Que
TarifaJubilat.nom()retorna"jubilat". - Que
GET /api/v1/estacions/99retorna unProblemDetailamb estat 404. - Que
EstacioResponseés unrecordamb sis components. - Que un ciutadà rep
403en finalitzar el lloguer d'un altre. - Que
findByUsuariIdAndFiIsNullretorna només lloguers sense data de fi. - Que la migració
V4crea l'índex sobrelloguers(inici). - Que
server.portval 8080. - 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,GestorGlobalExcepcionsi tot el paquetseguretat. 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íniaMath.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
- Què és Spring Boot?
- Configuració del teu entorn de desenvolupament
- Creant la teva primera aplicació Spring Boot
- Entenent l'estructura del projecte
- L'arrencada i el cicle de vida de l'aplicació
Mòdul 2: Conceptes bàsics de Spring Boot
- Anotacions de Spring Boot
- Injecció de dependències a Spring Boot
- Àmbit i cicle de vida dels beans
- Configuració de Spring Boot
- Propietats de Spring Boot
- Autoconfiguració i starters per dins
Mòdul 3: Construint serveis web RESTful
- Introducció als serveis web RESTful
- Creant controladors REST
- Gestió dels mètodes HTTP
- Validació de dades d'entrada
- DTOs i mapatge entre capes
- Gestió d'excepcions a REST
- Documentar l'API amb OpenAPI
Mòdul 4: Accés a dades amb Spring Boot
- Introducció a Spring Data JPA
- Configuració de fonts de dades
- Creació d'entitats JPA
- Relacions entre entitats
- Ús de repositoris de Spring Data
- Mètodes de consulta a Spring Data JPA
- Transaccions i gestió de la persistència
- Migracions d'esquema amb Flyway
Mòdul 5: Seguretat a Spring Boot
- Introducció a Spring Security
- Configuració de Spring Security
- Autenticació i autorització d'usuaris
- Implementació d'autenticació JWT
- Seguretat a nivell de mètode i enduriment de l'API
Mòdul 6: Proves a Spring Boot
- Introducció a les proves
- Proves unitàries amb JUnit
- Simulació amb Mockito
- Proves d'integració
- Proves amb Testcontainers
Mòdul 7: Funcions avançades de Spring Boot
- Spring Boot Actuator
- Perfils de Spring Boot
- Tasques programades i execució asíncrona
- Spring Boot amb Docker
- Spring Boot i microserveis
- Comunicació entre serveis i tolerància a fallades
Mòdul 8: Desplegament d'aplicacions Spring Boot
- Introducció al desplegament
- Desplegant a Heroku
- Desplegant a AWS
- Desplegant a Kubernetes
- Integració i lliurament continus
Mòdul 9: Rendiment i monitoratge
- Ajust de rendiment
- Memòria cau amb Spring Cache
- Monitoratge amb Spring Boot Actuator
- Ús de Prometheus i Grafana
- Gestió de registres i logs
- Traçabilitat distribuïda
