Onze lliçons del mòdul 10, tres del mòdul 11, una migració completa de CSV a base de dades relacional, un domini reescrit, genèrics, streams, java.time, classes segellades, fils virtuals, Spring i Hibernate.
I ni una sola prova automàtica.
Cada refactorització dels dos últims mòduls s'ha fet sense xarxa. Cada vegada que has canviat el càlcul de multes, l'única forma de saber si continuava funcionant ha estat arrencar l'aplicació, prestar un llibre, esperar i mirar la consola. Cada vegada que has tocat el límit de préstecs per empleat, has confiat que no vas trencar res.
Aquesta lliçó acaba amb això.
Una prova automàtica és un programa que executa el teu codi amb entrades conegudes i comprova que el resultat és l'esperat, sense que ningú miri. JUnit 5 és el framework estàndard per escriure-les en Java: descobreix els teus mètodes, els executa, informa de les fallades i s'integra amb Maven, amb l'IDE i amb la integració contínua.
I aquí es cobra per fi una decisió de disseny que portes dos mòduls arrossegant: aquell Clock que vas injectar a 10-05 "per poder provar el venciment sense esperar quinze dies", que va passar per Spring com un @Bean a 11-02 i que ha aparegut a cada LocalDate.now(rellotge) d'11-03. Avui el fas servir.
En acabar sabràs per què es prova i què et dóna a canvi; coneixeràs l'arquitectura de JUnit 5 i l'anatomia d'una prova; dominaràs les assercions de JUnit i les d'AssertJ; sabràs organitzar proves amb cicle de vida, imbricació i etiquetes; escriuràs proves parametritzades que cobreixen dotze casos en sis línies; i —el més important— entendràs què fa que un disseny sigui provable i quin codi senzillament no es pot provar.
BiblioTech tindrà la seva primera bateria de proves.
Contingut
- Per què provar: el cost de no tenir proves
- Què et donen a canvi
- Què és exactament una prova automàtica
- La piràmide de proves
- JUnit 5: arquitectura
- Les dependències al
pom.xml - La primera prova de BiblioTech
- Anatomia: Preparar-Actuar-Comprovar
- Noms de prova i
@DisplayName - Les assercions de JUnit
assertThrowsi les excepcionsassertAll: agrupar comprovacions- AssertJ: per què es recomana
- El cicle de vida:
@BeforeEachi companyia - Una instància per prova i
@TestInstance @Disabledi les anotacions condicionals- Proves parametritzades
@MethodSourcei els casos complexos@Nested: organitzar per escenari@Tagi l'execució selectiva@TempDir: provar el codi de fitxers- Què fa provable un disseny
- El
Clockinjectable, per fi cobrat - El codi que NO es pot provar
- Què és un bon conjunt de proves
- Antipatrons
- Provar codi concurrent
- Executar les proves amb Maven
- Cobertura, esmentada
@SpringBootTesti@DataJpaTest, presentats- La bateria de proves de BiblioTech
- Errors comuns i consells
- Exercicis
- Per què provar: el cost de no tenir proves
Comencem pel costat incòmode, amb BiblioTech com a exemple real.
A 11-03 vas convertir Material d'un record sealed a una classe abstracta amb @Entity. Vas canviar Optional<LocalDate> dataDevolucio per un camp anul·lable amb un getter que embolcalla. Vas substituir LectorCsv per JpaRepository. Vas introduir @Version i un bucle de reintents.
Pregunta: el càlcul de multes continua donant el mateix resultat que abans de la migració?
La resposta honesta avui és "crec que sí". No ho saps. Ningú ho sap.
Aquest "crec que sí" té cinc costos concrets:
| Cost | En què es tradueix |
|---|---|
| Por de canviar | El codi es podreix perquè ningú s'atreveix a tocar-lo. "Funciona, no ho toquis" |
| Regressions | Un arranjament trenca alguna cosa que funcionava, i es descobreix en producció |
| Depuració lenta | Sense proves, localitzar una fallada és arrencar l'aplicació i reproduir-la a mà |
| Verificació manual | Cada canvi exigeix repetir a mà les mateixes 20 comprovacions |
| Actualitzar dependències | Pujar de Spring Boot 3.3 a 3.4 es converteix en un projecte de tres setmanes |
Aquest últim mereix atenció, perquè enllaça directament amb Log4Shell (11-01): la capacitat de respondre ràpid a una vulnerabilitat depèn de tenir proves. Sense elles, canviar una versió requereix una campanya de proves manuals, i la teva finestra d'exposició es mesura en setmanes.
I hi ha un cost més subtil: el codi sense proves tendeix a ser pitjor codi. No perquè l'autor sigui pitjor, sinó perquè res no l'obliga que les seves classes es puguin construir aïlladament. Un static que llegeix el rellotge del sistema, un new dins d'un mètode, una classe amb nou dependències: tot això passa desapercebut fins que intentes provar-ho. Les proves són un detector de problemes de disseny.
- Què et donen a canvi
| Benefici | Explicació |
|---|---|
| Refactoritzar sense por | Canvies la implementació, executes les proves, saps si vas trencar alguna cosa. En segons |
| Documentació executable | multaDeCincDiesDeRetardEsDosEurosCinquanta documenta la regla i no es pot desactualitzar, perquè si menteix, falla |
| Regressions detectades | La fallada apareix a l'instant, no en producció |
| Depuració més ràpida | Una prova que falla acota el problema a un mètode |
| Millor disseny | Provar obliga a injectar dependències, aïllar efectes i separar responsabilitats |
| Confiança per desplegar | La integració contínua executa 400 proves en 20 segons abans de cada desplegament |
Sobre la documentació executable, un exemple de BiblioTech. Això:
@Test
void laMultaEsLimitaAlMaximConfigurat() {
Prestec prestec = prestecAmbRetard(200); // 200 dies x 0,50 EUR = 100 EUR
assertThat(calculadora.calcular(prestec)).isEqualByComparingTo("20.00");
}comunica una regla de negoci millor que qualsevol comentari, i amb una propietat que cap comentari no té: si algú canvia el comportament sense canviar la regla, la prova es posa vermella.
- Què és exactament una prova automàtica
Una prova automàtica és codi que:
- Prepara un escenari conegut.
- Executa el codi que es vol verificar.
- Comprova que el resultat és l'esperat.
- Falla sorollosament si no ho és.
Comparat amb el que has fet fins ara:
| Verificació manual (mòduls 1-11) | Prova automàtica | |
|---|---|---|
| Qui comprova | Un humà mirant la consola | El framework |
| Quan | Quan algú se'n recorda | A cada compilació |
| Cost de repetir-la | Minuts d'una persona | Mil·lisegons |
| Casos límit | Els que a algú se li acudeixen | Tots els que vas escriure, sempre |
| S'oblida? | Sí | No |
| Resultat | "Sembla que va bé" | Verd o vermell |
- La piràmide de proves
No totes les proves són iguals. La piràmide de proves descriu la proporció sana entre els tipus.
graph TD
E2E["EXTREM A EXTREM<br/>Poques · Lentes (segons-minuts) · Fragils<br/>Tota l'aplicacio desplegada"]
INT["INTEGRACIO<br/>Algunes · Mitjanes (centenars de ms)<br/>Diverses peces juntes: BD, HTTP, context de Spring"]
UNIT["UNITARIES<br/>MOLTES · Rapides (mil.lisegons) · Estables<br/>Una classe, sense dependencies externes"]
E2E --- INT
INT --- UNIT
| Tipus | Què prova | Velocitat | Quantes | A BiblioTech |
|---|---|---|---|---|
| Unitària | Una classe aïllada | 1-10 ms | Moltes (70-80 %) | CalculadoraMultes, regles de Prestec |
| Integració | Diverses peces reals juntes | 100 ms - 2 s | Algunes (15-25 %) | PrestecRepository contra H2, @SpringBootTest |
| Extrem a extrem | El sistema complet | Segons o minuts | Poques (5 %) | Prestar un llibre per l'API REST (12-05) |
Per què la forma importa: si inverteixes la piràmide i gairebé totes les teves proves són d'integració, el teu conjunt triga vint minuts, falla de forma intermitent i ningú l'executa. Si és piramidal, triga vint segons i s'executa a cada desat.
Aquesta lliçó se centra en la base: proves unitàries del domini de BiblioTech. La integració es presenta al final i es desenvolupa a 11-06 i 12-05.
- JUnit 5: arquitectura
JUnit 5 no és un artefacte, sinó tres subprojectes. Saber-ho evita molta confusió amb les dependències.
graph TD
A["JUnit 5 (JUnit Platform + Jupiter + Vintage)"] --> B["JUnit Platform<br/>Motor de descobriment i execucio.<br/>Es el que fan servir Maven, Gradle i els IDE"]
A --> C["JUnit Jupiter<br/>L'API que tu escrius:<br/>@Test, @BeforeEach, Assertions...<br/>+ el seu motor d'execucio"]
A --> D["JUnit Vintage<br/>Motor que executa proves de JUnit 3 i 4<br/>(nomes per a codi heretat)"]
B --- C
B --- D
| Component | Què és | El fas servir |
|---|---|---|
| Platform | Infraestructura de descobriment i execució | Indirectament |
| Jupiter | L'API i el motor de JUnit 5 | Directament: és el que escrius |
| Vintage | Compatibilitat amb JUnit 4 | Només si migres codi antic |
Diferències amb JUnit 4, útils si veus codi antic:
| JUnit 4 | JUnit 5 (Jupiter) |
|---|---|
org.junit.Test |
org.junit.jupiter.api.Test |
@Before / @After |
@BeforeEach / @AfterEach |
@BeforeClass / @AfterClass |
@BeforeAll / @AfterAll |
@Ignore |
@Disabled |
@RunWith / @Rule |
@ExtendWith (model unificat d'extensions) |
expected = X.class |
assertThrows(X.class, ...) |
Classes i mètodes public obligatoris |
Pot ser visibilitat de paquet |
- Les dependències al
pom.xml
pom.xmlAmb Spring Boot, una sola dependència ho porta tot:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>mvn dependency:tree revela què arrossega:
| Artefacte | Per a què |
|---|---|
junit-jupiter |
JUnit 5: API i motor |
assertj-core |
Assercions fluides (apartat 13) |
mockito-core, mockito-junit-jupiter |
Dobles de prova (11-06) |
spring-test, spring-boot-test |
@SpringBootTest, MockMvc |
hamcrest |
Matchers (menys usat avui) |
jsonassert, json-path |
Assercions sobre JSON |
xmlunit-core |
Assercions sobre XML |
Fixa't en <scope>test</scope>: aquestes llibreries no s'empaqueten al jar final. És l'àmbit test de Maven, que s'explica a 11-05.
Sense Spring, les dependències mínimes serien:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.25.3</version>
<scope>test</scope>
</dependency>I l'estructura de directoris que Maven imposa (11-05):
src/
├── main/java/com/nexussoftware/bibliotech/ <- codi de produccio
├── main/resources/ <- application.yml
├── test/java/com/nexussoftware/bibliotech/ <- LES PROVES, mateix paquet
└── test/resources/ <- application-test.yml, fitxers de provaLa convenció de posar la prova al mateix paquet que la classe provada té una conseqüència útil: la prova pot accedir als membres amb visibilitat de paquet sense necessitat de fer-los públics.
- La primera prova de BiblioTech
La classe que provarem, tal com va quedar a 11-02:
package com.nexussoftware.bibliotech.servei;
import java.math.BigDecimal;
import java.time.*;
import java.time.temporal.ChronoUnit;
import org.springframework.stereotype.Service;
@Service
public class CalculadoraMultes {
private final PropietatsBiblioTech propietats;
private final Clock rellotge;
public CalculadoraMultes(PropietatsBiblioTech propietats, Clock rellotge) {
this.propietats = propietats;
this.rellotge = rellotge;
}
public BigDecimal calcular(Prestec prestec) {
if (prestec.getDataDevolucio().isPresent()) {
return BigDecimal.ZERO; // ja retornat: sense multa
}
LocalDate avui = LocalDate.now(rellotge);
long diesRetard = ChronoUnit.DAYS.between(prestec.getDataVenciment(), avui);
if (diesRetard <= 0) {
return BigDecimal.ZERO; // encara en termini
}
BigDecimal multa = propietats.multa().eurosPerDia()
.multiply(BigDecimal.valueOf(diesRetard));
return multa.min(propietats.multa().maxima());
}
}I la seva primera prova, a src/test/java/com/nexussoftware/bibliotech/servei/CalculadoraMultesTest.java:
package com.nexussoftware.bibliotech.servei;
import static org.assertj.core.api.Assertions.assertThat;
import java.math.BigDecimal;
import java.time.*;
import org.junit.jupiter.api.Test;
class CalculadoraMultesTest {
@Test
void unPrestecAmbCincDiesDeRetardGeneraDosEurosCinquanta() {
// PREPARAR: un rellotge FIX. "Avui" es sempre el 20 de marc de 2026.
Clock rellotgeFix = Clock.fixed(
Instant.parse("2026-03-20T10:00:00Z"), ZoneId.of("Europe/Madrid"));
PropietatsBiblioTech propietats = propietatsDeProva(
new BigDecimal("0.50"), new BigDecimal("20.00"));
CalculadoraMultes calculadora = new CalculadoraMultes(propietats, rellotgeFix);
// Vencia el 15 -> 5 dies de retard respecte al 20
Prestec prestec = new Prestec(unLlibre(), unEmpleat(),
LocalDate.of(2026, 3, 1), LocalDate.of(2026, 3, 15));
// ACTUAR
BigDecimal multa = calculadora.calcular(prestec);
// COMPROVAR
assertThat(multa).isEqualByComparingTo("2.50");
}
}Atura't en una cosa important: no hi ha Spring enlloc. No hi ha @SpringBootTest, no hi ha context, no hi ha base de dades. Es construeix la classe amb new, se li passen les seves dependències, es crida un mètode i es comprova el resultat. Triga menys d'un mil·lisegon.
Això és possible exactament perquè a 11-02 es va triar injecció per constructor i a 10-05 es va injectar el Clock en comptes de cridar LocalDate.now(). Totes les decisions de disseny dels últims mòduls es cobren en aquesta línia.
Executar-la:
- Anatomia: Preparar-Actuar-Comprovar
Tota prova ben escrita té tres fases, i convé separar-les visualment. El patró es coneix com a AAA (Arrange-Act-Assert) o Given-When-Then.
@Test
void unEmpleatNoPotSuperarElLimitDePrestecs() {
// PREPARAR (Arrange): muntar l'escenari
Empleat marta = new Empleat("[email protected]", "Marta Ruiz");
GestorPrestecs gestor = new GestorPrestecs(repositori, calculadora, avisos, propietats, rellotge);
gestor.prestar("978-0000000001", marta.getCorreu());
gestor.prestar("978-0000000002", marta.getCorreu());
gestor.prestar("978-0000000003", marta.getCorreu());
// ACTUAR (Act): UNA sola accio, la que s'esta provant
ThrowingCallable quartPrestec =
() -> gestor.prestar("978-0000000004", marta.getCorreu());
// COMPROVAR (Assert): verificar el resultat
assertThatThrownBy(quartPrestec)
.isInstanceOf(LimitPrestecsException.class)
.hasMessageContaining("[email protected]");
}Regles del patró:
- Una sola acció per prova. Si n'hi ha dues, són dues proves.
- Les fases se separen amb una línia en blanc o un comentari.
- Sense lògica a la prova: res d'
if,fornitry/catchimprovisats. Una prova amb lògica és codi que també pot tenir bugs, i no hi ha proves per a les proves. - Una prova comprova un comportament, no un mètode. Un mètode amb tres regles necessita tres proves.
- Noms de prova i
@DisplayName
@DisplayNameEl nom d'una prova és documentació. Quan falla a la integració contínua, el primer —de vegades l'únic— que es llegeix és el seu nom.
| Mal nom | Bon nom |
|---|---|
test1 |
laMultaEsZeroSiElPrestecEstaEnTermini |
testCalcular |
laMultaEsLimitaAlMaximConfigurat |
testMulta |
unPrestecRetornatNoGeneraMulta |
testException |
prestarUnMaterialSenseExemplarsLlancaSenseExemplarsException |
Una fórmula útil: condició + resultatEsperat, o l'estil should: hauriaDeLimitarLaMultaAlMaxim.
I per a prosa completa, @DisplayName:
@DisplayName("Càlcul de multes per retard")
class CalculadoraMultesTest {
@Test
@DisplayName("Un préstec amb 5 dies de retard genera una multa de 2,50 €")
void multaDeCincDies() { ... }
@Test
@DisplayName("La multa mai no supera el màxim configurat (20 €)")
void multaLimitadaAlMaxim() { ... }
}La sortida de l'IDE i dels informes es torna llegible per a qualsevol:
Càlcul de multes per retard
✔ Un préstec amb 5 dies de retard genera una multa de 2,50 €
✔ La multa mai no supera el màxim configurat (20 €)
- Les assercions de JUnit
org.junit.jupiter.api.Assertions (habitualment amb import static):
| Asserció | Comprova | Exemple |
|---|---|---|
assertEquals(esperat, real) |
Igualtat amb equals |
assertEquals(3, gestor.comptarActius()) |
assertNotEquals(a, b) |
Desigualtat | |
assertTrue(cond) / assertFalse(cond) |
Condició booleana | assertTrue(prestec.estaVencut(avui)) |
assertNull(o) / assertNotNull(o) |
Nul·litat | |
assertSame(a, b) / assertNotSame(a, b) |
Identitat de referència (==) |
|
assertArrayEquals(a, b) |
Arrays element a element | |
assertIterableEquals(a, b) |
Iterables en ordre | |
assertLinesMatch(a, b) |
Línies, amb expressions regulars | |
assertThrows(C.class, exec) |
Que es llança una excepció | |
assertDoesNotThrow(exec) |
Que no se'n llança cap | |
assertTimeout(dur, exec) |
Que acaba a temps | |
assertAll(...) |
Agrupa diverses assercions | |
fail("motiu") |
Falla incondicionalment | Branca que no s'hauria d'assolir |
Compte amb l'ordre dels arguments: l'esperat va primer. Invertir-ho no canvia el resultat, però produeix missatges d'error enganyosos ("expected 5 but was 3" quan era al revés).
I afegeix sempre un missatge quan l'asserció no sigui òbvia:
assertEquals(3, gestor.comptarActius(marta),
"Marta hauria de tenir 3 prestecs actius despres de prestar tres materials");Aquest missatge pot ser un Supplier<String> perquè només es construeixi si la prova falla:
assertTrue(prestec.estaVencut(avui),
() -> "El prestec vencia el " + prestec.getDataVenciment() + " i avui es " + avui);
assertThrows i les excepcions
assertThrows i les excepcionsProvar que alguna cosa falla com cal és tan important com provar que funciona.
@Test
void prestarUnMaterialSenseExemplarsLlancaExcepcio() {
Material exhaurit = new Llibre("978-0000000003", "Refactoritzacio", 0, "M. Fowler");
// assertThrows RETORNA l'excepcio: es pot inspeccionar
SenseExemplarsException excepcio = assertThrows(
SenseExemplarsException.class,
() -> exhaurit.prestarUnExemplar());
assertEquals("978-0000000003", excepcio.getIsbn());
assertTrue(excepcio.getMessage().contains("978-0000000003"));
}Tres detalls que importen:
- Retorna l'excepció, així que se'n poden comprovar el missatge, la causa i els camps propis. Aprofita-ho: comprovar només el tipus és una prova feble.
- El segon argument és un
Executable, una interfície funcional. Per això es passa una lambda (mòdul 4). - Accepta subclasses.
assertThrows(BiblioTechException.class, ...)passa si es llançaSenseExemplarsException. Si necessites el tipus exacte, fes servirassertThrowsExactly.
I l'error que cal evitar:
// MALAMENT: si NO llanca l'excepcio, la prova passa igualment
@Test
void malament() {
try {
exhaurit.prestarUnExemplar();
} catch (SenseExemplarsException e) {
assertEquals("978-0000000003", e.getIsbn());
}
// Si no es llanca res, no s'executa cap catch... i la prova es verda.
}
assertAll: agrupar comprovacions
assertAll: agrupar comprovacionsProblema: si una prova té cinc assercions i falla la primera, no arribes a veure les altres quatre. Arregles una cosa, executes, falla la següent. Cinc iteracions per veure tota la informació.
assertAll executa totes les assercions i reporta totes les fallades:
@Test
void unPrestecNouTeLesDadesCorrectes() {
LocalDate avui = LocalDate.of(2026, 3, 1);
Prestec prestec = new Prestec(unLlibre(), unEmpleat(), avui, 15);
assertAll("dades del prestec acabat de crear",
() -> assertEquals(avui, prestec.getDataPrestec()),
() -> assertEquals(avui.plusDays(15), prestec.getDataVenciment()),
() -> assertEquals(EstatPrestec.ACTIU, prestec.getEstat()),
() -> assertTrue(prestec.getDataDevolucio().isEmpty()),
() -> assertNotNull(prestec.getMaterial()));
}Si en fallen dues, l'informe mostra les dues. Fes-lo servir quan comprovis diverses propietats del mateix resultat; no el facis servir per ficar tres comportaments diferents en una prova.
- AssertJ: per què es recomana
AssertJ ofereix assercions fluides, i ve inclòs a spring-boot-starter-test. La diferència principal no és estètica: són els missatges d'error.
Expected size: 3 but was: 2 in:
[Prestec{isbn='978-0000000001', empleat='Marta Ruiz', venc=2026-03-15},
Prestec{isbn='978-0000000002', empleat='Marta Ruiz', venc=2026-03-16}]El segon et diu què hi havia, no només quants. En una prova que falla a la integració contínua a les onze de la nit, aquesta diferència és enorme.
Comparativa:
| Comprovació | JUnit | AssertJ |
|---|---|---|
| Igualtat | assertEquals(a, b) |
assertThat(b).isEqualTo(a) |
| Mida de col·lecció | assertEquals(3, l.size()) |
assertThat(l).hasSize(3) |
| Conté | assertTrue(l.contains(x)) |
assertThat(l).contains(x) |
| Conté exactament | Bucle a mà | assertThat(l).containsExactly(a, b, c) |
| Buit | assertTrue(l.isEmpty()) |
assertThat(l).isEmpty() |
| Cadena conté | assertTrue(s.contains("x")) |
assertThat(s).contains("x") |
BigDecimal sense escala |
assertEquals(0, a.compareTo(b)) |
assertThat(a).isEqualByComparingTo("2.50") |
Optional amb valor |
assertTrue(o.isPresent()) && ... |
assertThat(o).contains(x) |
| Excepció | assertThrows(...) |
assertThatThrownBy(...).isInstanceOf(...) |
| Extreure un camp | Bucle a mà | assertThat(l).extracting("titol").contains("...") |
Exemples que mostren la seva expressivitat:
import static org.assertj.core.api.Assertions.*;
// Encadenament sobre col.leccions
assertThat(prestecsVencuts)
.hasSize(2)
.extracting(Prestec::getEmpleat)
.extracting(Empleat::getNom)
.containsExactlyInAnyOrder("Marta Ruiz", "Diego Alonso");
// BigDecimal: compara VALOR, no escala. 2.50 vs 2.5 no falla.
assertThat(multa).isEqualByComparingTo("2.50");
// Optional (10-04)
assertThat(repositori.cercarPerIsbn("978-0000000001"))
.isPresent()
.get()
.extracting(Material::getTitol)
.isEqualTo("Java Eficac");
// Excepcions, amb mes detall
assertThatThrownBy(() -> gestor.prestar("978-9999999999", "[email protected]"))
.isInstanceOf(MaterialNoTrobatException.class)
.hasMessageContaining("978-9999999999")
.hasNoCause();
// Comparar objectes camp a camp, ignorant l'id generat
assertThat(prestecDesat)
.usingRecursiveComparison()
.ignoringFields("id", "version")
.isEqualTo(prestecEsperat);
// Predicats sobre tots els elements
assertThat(prestecsActius)
.isNotEmpty()
.allMatch(p -> p.getDataDevolucio().isEmpty())
.allSatisfy(p -> assertThat(p.getDataVenciment()).isAfter(p.getDataPrestec()));Aquesta última línia, isEqualByComparingTo, resol un problema molt real: new BigDecimal("2.50").equals(new BigDecimal("2.5")) és false, perquè equals de BigDecimal compara també l'escala. Amb assertEquals això produeix fallades desconcertants. AssertJ té el mètode correcte.
Recomanació clara: fes servir AssertJ. Ja és al teu classpath, els missatges són millors i l'autocompletat de l'IDE et va guiant (assertThat(llista). i mira què t'ofereix). Fes servir les de JUnit per a assertThrows si prefereixes inspeccionar l'excepció retornada, encara que assertThatThrownBy cobreix gairebé tot.
- El cicle de vida:
@BeforeEach i companyia
@BeforeEach i companyiaQuan diverses proves comparteixen preparació, hi ha ganxos de cicle de vida.
class GestorPrestecsTest {
private Clock rellotgeFix;
private CalculadoraMultes calculadora;
private GestorPrestecs gestor;
private Empleat marta;
@BeforeAll
static void prepararTotaLaClasse() {
// UNA vegada, abans de totes les proves. static (llevat d'amb @TestInstance PER_CLASS)
// Per a recursos cars i COMPARTITS: arrencar un contenidor, carregar un fitxer gran.
}
@BeforeEach
void prepararCadaProva() {
// ABANS DE CADA prova: estat net, sense contaminacio entre proves
rellotgeFix = Clock.fixed(Instant.parse("2026-03-20T10:00:00Z"), ZoneId.of("Europe/Madrid"));
calculadora = new CalculadoraMultes(propietatsDeProva(), rellotgeFix);
gestor = new GestorPrestecs(new RepositoriEnMemoria(), calculadora,
new AvisosEnMemoria(), propietatsDeProva(), rellotgeFix);
marta = new Empleat("[email protected]", "Marta Ruiz");
}
@AfterEach
void netejarCadaProva() {
// Despres de cada prova. Amb recursos gestionats, rarament necessari.
}
@AfterAll
static void netejarTotaLaClasse() {
// Una vegada, al final. Tancar el que s'ha obert a @BeforeAll.
}
}| Anotació | Quan | Freqüència | static |
|---|---|---|---|
@BeforeAll |
Abans de tot | 1 vegada | Sí (per defecte) |
@BeforeEach |
Abans de cada prova | N vegades | No |
@AfterEach |
Després de cada prova | N vegades | No |
@AfterAll |
Al final de tot | 1 vegada | Sí (per defecte) |
Amb herència, l'ordre és: @BeforeAll de la superclasse → @BeforeAll de la subclasse → @BeforeEach de la superclasse → @BeforeEach de la subclasse → prova → @AfterEach subclasse → @AfterEach superclasse → ...
Consell important: prefereix @BeforeEach a @BeforeAll. L'estat compartit entre proves és la causa número u de proves que depenen de l'ordre d'execució. @BeforeAll només per a recursos cars i immutables.
I un avís sobre l'excés: si @BeforeEach té trenta línies i cada prova en fa servir una part diferent, la preparació es torna il·legible ("mystery guest": no se sap d'on surten les dades). En aquest cas, millor mètodes de fàbrica a la mateixa classe de prova:
private Prestec prestecAmbRetardDe(int dies) {
LocalDate avui = LocalDate.now(rellotgeFix);
return new Prestec(unLlibre(), marta, avui.minusDays(15 + dies), avui.minusDays(dies));
}
private Material unLlibre() {
return new Llibre("978-0000000001", "Java Eficac", 3, "J. Bloch");
}Així cada prova diu exactament quin escenari fa servir: prestecAmbRetardDe(5).
- Una instància per prova i
@TestInstance
@TestInstanceComportament per defecte que sorprèn molta gent: JUnit crea una instància nova de la classe de prova per a cada mètode @Test.
class ComptadorTest {
private int comptador = 0; // es reinicia a CADA prova
@Test void primera() { comptador++; assertEquals(1, comptador); } // passa
@Test void segona() { comptador++; assertEquals(1, comptador); } // TAMBE passa
}És deliberat: garanteix que les proves són independents. Un camp modificat en una prova no pot afectar-ne una altra.
Conseqüència: @BeforeAll i @AfterAll han de ser static, perquè no pertanyen a cap instància. Si necessites que no ho siguin:
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class CatalegTest {
private List<Material> cataleg; // compartit per TOTES les proves
@BeforeAll
void carregarCataleg() { // ja no cal que sigui static
cataleg = carregarCatalegGran();
}
}Fes-lo servir amb cura: en compartir instància, tornes a poder contaminar unes proves amb altres. El seu ús legítim és evitar recarregar un recurs car i immutable.
@Disabled i les anotacions condicionals
@Disabled i les anotacions condicionals@Test
@Disabled("Pendent de decidir la politica de multes per a revistes (BIB-142)")
void multaDeRevistaEsLaMeitat() { ... }Regla professional: @Disabled sempre amb motiu i amb referència. Una prova desactivada sense explicació és brossa que ningú s'atrevirà a esborrar d'aquí a un any. I una prova desactivada durant mesos és una prova que cal esborrar: està donant una falsa sensació de cobertura.
Condicionals, per a proves que només tenen sentit en certs entorns:
@Test
@EnabledOnOs(OS.LINUX)
void caminsDePermisosPosix() { ... }
@Test
@DisabledOnOs(OS.WINDOWS)
void enllacosSimbolics() { ... }
@Test
@EnabledOnJre(JRE.JAVA_21)
void filsVirtuals() { ... } // repren 10-06
@Test
@EnabledIfSystemProperty(named = "proves.integracio", matches = "true")
void contraLaBaseDeDadesReal() { ... }
@Test
@EnabledIfEnvironmentVariable(named = "CI", matches = "true")
void nomesEnIntegracioContinua() { ... }La diferència amb @Disabled és important: una prova condicional s'executa quan es compleixen les condicions; una @Disabled no s'executa mai.
- Proves parametritzades
Aquí hi ha una de les millors característiques de JUnit 5. Aquest codi fa mala olor:
@Test void multaDUnDia() { assertThat(calcular(1)).isEqualByComparingTo("0.50"); }
@Test void multaDeDosDies() { assertThat(calcular(2)).isEqualByComparingTo("1.00"); }
@Test void multaDeCincDies() { assertThat(calcular(5)).isEqualByComparingTo("2.50"); }
@Test void multaDeDeuDies() { assertThat(calcular(10)).isEqualByComparingTo("5.00"); }
// ... vuit mesAmb @ParameterizedTest:
@ParameterizedTest(name = "{0} dies de retard -> {1} EUR")
@CsvSource({
" 1, 0.50",
" 2, 1.00",
" 5, 2.50",
" 10, 5.00",
" 39, 19.50",
" 40, 20.00", // just el maxim
" 41, 20.00", // passat el maxim: es limita
"200, 20.00", // molt passat: continua limitat
" 0, 0.00", // venc avui: sense retard
" -1, 0.00", // venc dema
" -5, 0.00",
"-30, 0.00"
})
void laMultaEsCalculaSegonsElsDiesDeRetard(int diesRetard, BigDecimal multaEsperada) {
Prestec prestec = prestecAmbRetardDe(diesRetard);
assertThat(calculadora.calcular(prestec)).isEqualByComparingTo(multaEsperada);
}Dotze casos en sis línies, incloent-hi els tres casos límit que de veritat importen (39, 40, 41 al voltant del màxim) i els negatius. I la sortida és llegible:
laMultaEsCalculaSegonsElsDiesDeRetard
✔ 1 dies de retard -> 0.50 EUR
✔ 5 dies de retard -> 2.50 EUR
✔ 40 dies de retard -> 20.00 EUR
✔ 41 dies de retard -> 20.00 EUR
...Les fonts de dades disponibles:
| Anotació | Proporciona | Exemple |
|---|---|---|
@ValueSource |
Un array de literals | @ValueSource(ints = {1, 5, 10}) |
@CsvSource |
Diverses columnes en línia | @CsvSource({"5, 2.50"}) |
@CsvFileSource |
Un CSV de src/test/resources |
@CsvFileSource(resources = "/multes.csv") |
@EnumSource |
Constants d'un enum | @EnumSource(EstatPrestec.class) |
@MethodSource |
Un mètode que retorna un Stream |
Objectes complexos |
@NullSource, @EmptySource, @NullAndEmptySource |
null i buit |
Validació d'entrades |
@ArgumentsSource |
Un proveïdor propi | Casos molt elaborats |
Exemples de les més útils:
// Tots els estats de l'enum (04-07)
@ParameterizedTest
@EnumSource(EstatPrestec.class)
void totEstatTeDescripcioNoBuida(EstatPrestec estat) {
assertThat(estat.getDescripcio()).isNotBlank();
}
// Nomes alguns
@ParameterizedTest
@EnumSource(value = EstatPrestec.class, names = {"ACTIU", "VENCUT"})
void elsEstatsNoFinalitzatsPermetenDevolucio(EstatPrestec estat) {
assertThat(estat.permetDevolucio()).isTrue();
}
// Validacio d'entrades invalides: null, buit i brossa
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = { " ", "abc", "978", "978-000000000X", "1234567890123" })
void unIsbnInvalidEsRebutjat(String isbnInvalid) {
assertThatThrownBy(() -> new Isbn(isbnInvalid))
.isInstanceOf(IsbnInvalidException.class);
}Aquest últim patró —combinar @NullAndEmptySource amb @ValueSource— és la forma canònica de provar validació d'entrada, i cobreix en quatre línies els casos que normalment s'obliden.
@MethodSource i els casos complexos
@MethodSource i els casos complexosQuan els paràmetres són objectes i no literals:
class GestorPrestecsTest {
// El metode ha de ser static (o la classe, @TestInstance(PER_CLASS))
static Stream<Arguments> escenarisDePrestec() {
return Stream.of(
Arguments.of("Empleat sense prestecs, material disponible",
0, 3, true, null),
Arguments.of("Empleat al limit",
3, 3, false, LimitPrestecsException.class),
Arguments.of("Material sense exemplars",
1, 0, false, SenseExemplarsException.class),
Arguments.of("Empleat a un prestec del limit",
2, 5, true, null)
);
}
@ParameterizedTest(name = "{0}")
@MethodSource("escenarisDePrestec")
void escenarisDePrestecDeBiblioTech(String descripcio,
int prestecsActius,
int exemplarsDisponibles,
boolean hauriaDeTenirExit,
Class<? extends Exception> excepcioEsperada) {
// PREPARAR
Material material = new Llibre("978-0000000001", "Java Eficac",
exemplarsDisponibles, "J. Bloch");
Empleat marta = new Empleat("[email protected]", "Marta Ruiz");
repositori.desar(material);
repositori.desar(marta);
for (int i = 0; i < prestecsActius; i++) {
repositori.desarPrestec(new Prestec(unAltreLlibre(i), marta, avui(), 15));
}
// ACTUAR i COMPROVAR
if (hauriaDeTenirExit) {
assertThat(gestor.prestar("978-0000000001", marta.getCorreu())).isNotNull();
} else {
assertThatThrownBy(() -> gestor.prestar("978-0000000001", marta.getCorreu()))
.isInstanceOf(excepcioEsperada);
}
}
}Aquí hi ha un compromís que convé reconèixer: aquesta prova té un if, i l'apartat 8 deia que les proves no han de tenir lògica. És un cas admissible perquè la lògica és trivial i el guany en cobertura d'escenaris és gran, però si creix cal separar-la en dues proves parametritzades, una d'èxit i una altra de fallada.
@MethodSource també accepta un Stream simple quan només hi ha un paràmetre:
static Stream<Material> materialsDeProva() {
return Stream.of(
new Llibre("978-0000000001", "Java Eficac", 3, "J. Bloch"),
new Revista("978-0000000010", "Java Magazine", 5, 42),
new Dvd("978-0000000020", "Curs de Java", 1, 180));
}
@ParameterizedTest
@MethodSource("materialsDeProva")
void totMaterialEsPotPrestarSiHiHaExemplars(Material material) {
int abans = material.getExemplarsDisponibles();
material.prestarUnExemplar();
assertThat(material.getExemplarsDisponibles()).isEqualTo(abans - 1);
}
@Nested: organitzar per escenari
@Nested: organitzar per escenariQuan una classe de prova creix, @Nested permet agrupar per escenari amb preparació pròpia:
@DisplayName("GestorPrestecs")
class GestorPrestecsTest {
private GestorPrestecs gestor;
private Empleat marta;
@BeforeEach
void prepararComu() {
gestor = crearGestor();
marta = new Empleat("[email protected]", "Marta Ruiz");
}
@Nested
@DisplayName("quan l'empleat no té préstecs actius")
class SensePrestecsActius {
@Test
@DisplayName("pot prestar un material disponible")
void potPrestar() {
Prestec prestec = gestor.prestar("978-0000000001", marta.getCorreu());
assertThat(prestec).isNotNull();
}
@Test
@DisplayName("la data de venciment són 15 dies després")
void vencimentCorrecte() {
Prestec prestec = gestor.prestar("978-0000000001", marta.getCorreu());
assertThat(prestec.getDataVenciment())
.isEqualTo(LocalDate.now(rellotgeFix).plusDays(15));
}
}
@Nested
@DisplayName("quan l'empleat està al límit de préstecs")
class AlLimit {
@BeforeEach
void prestarFinsAlLimit() { // preparacio PROPIA d'aquest escenari
gestor.prestar("978-0000000001", marta.getCorreu());
gestor.prestar("978-0000000002", marta.getCorreu());
gestor.prestar("978-0000000003", marta.getCorreu());
}
@Test
@DisplayName("no pot prestar més")
void noPotPrestarMes() {
assertThatThrownBy(() -> gestor.prestar("978-0000000004", marta.getCorreu()))
.isInstanceOf(LimitPrestecsException.class);
}
@Test
@DisplayName("pot prestar de nou després de retornar-ne un")
void potDespresDeRetornar() {
gestor.retornar("978-0000000001", marta.getCorreu());
assertThat(gestor.prestar("978-0000000004", marta.getCorreu())).isNotNull();
}
}
}Els @BeforeEach s'acumulen: primer el de la classe externa, després el de la imbricada. La sortida és un informe que es llegeix com una especificació:
GestorPrestecs
quan l'empleat no té préstecs actius
✔ pot prestar un material disponible
✔ la data de venciment són 15 dies després
quan l'empleat està al límit de préstecs
✔ no pot prestar més
✔ pot prestar de nou després de retornar-ne unNota tècnica: les classes @Nested han de ser internes no estàtiques (classes internes, 04-03), precisament per poder accedir a l'estat de la classe externa.
@Tag i l'execució selectiva
@Tag i l'execució selectivaEtiquetar proves permet executar subconjunts:
@Tag("rapida")
class CalculadoraMultesTest { ... }
@Tag("lenta")
@Tag("integracio")
class PrestecRepositoryIT { ... }mvn test -Dgroups="rapida" # nomes les rapides
mvn test -DexcludedGroups="lenta,integracio" # tot menys el lentO al pom.xml (11-05):
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<excludedGroups>integracio</excludedGroups>
</configuration>
</plugin>Ús típic: a cada desat s'executen les unitàries (segons); a la integració contínua, tot. És la separació entre surefire i failsafe que veuràs a 11-05.
@TempDir: provar el codi de fitxers
@TempDir: provar el codi de fitxersBiblioTech té tot el codi d'E/S del mòdul 7: LectorCsv, EscriptorCsv, EscripturaAtomica, ImportadorCataleg. Mai no es va provar. @TempDir ho fa trivial:
class EscripturaAtomicaTest {
@TempDir
Path directoriTemporal; // JUnit el crea abans i l'ESBORRA despres, sol
@Test
void escriuElFitxerCompletONoL_Escriu() throws IOException {
Path desti = directoriTemporal.resolve("cataleg.csv");
EscripturaAtomica escriptura = new EscripturaAtomica();
escriptura.escriure(desti, List.of(
"isbn;titol;exemplars",
"978-0000000001;Java Eficac;3"));
assertThat(desti).exists();
assertThat(Files.readAllLines(desti, StandardCharsets.UTF_8))
.containsExactly(
"isbn;titol;exemplars",
"978-0000000001;Java Eficac;3");
}
@Test
void noDeixaFitxersTemporalsDespresDUnaEscripturaCorrecta() throws IOException {
Path desti = directoriTemporal.resolve("cataleg.csv");
new EscripturaAtomica().escriure(desti, List.of("linia"));
try (Stream<Path> fitxers = Files.list(directoriTemporal)) {
assertThat(fitxers).containsExactly(desti); // ni .tmp ni .bak
}
}
@Test
void elFitxerOriginalSobreviuSiLEscripturaFalla() throws IOException {
Path desti = directoriTemporal.resolve("cataleg.csv");
Files.writeString(desti, "contingut original", StandardCharsets.UTF_8);
assertThatThrownBy(() -> new EscripturaAtomica().escriure(desti, liniesQueFallen()))
.isInstanceOf(PersistenciaException.class);
// La garantia de l'escriptura atomica: l'original intacte
assertThat(Files.readString(desti, StandardCharsets.UTF_8))
.isEqualTo("contingut original");
}
}Avantatges de @TempDir enfront d'escriure a /tmp a mà:
- Es neteja sol, fins i tot si la prova falla.
- És únic per prova: dues proves no es trepitgen.
- Funciona en qualsevol sistema operatiu, sense camins escrits a foc.
- Es pot compartir per classe amb
@TempDir static Path.
Aquesta tercera prova és interessant: verifica la propietat que va motivar escriure EscripturaAtomica a 07-06 —que una fallada a mitges no corrompi el fitxer existent—. Això és provar comportament, no implementació.
- Què fa provable un disseny
Aquest és l'apartat que més canviarà la teva forma d'escriure codi.
La provabilitat no s'afegeix després: és una propietat del disseny. I portes dos mòduls prenent decisions que apuntaven aquí sense que es digués del tot.
- Injecció de dependències
// NO PROVABLE: crea les seves dependencies
public class GestorPrestecs {
private final Repositori repo = new RepositoriJpa(); // necessita base de dades
private final ServeiAvisos avisos = new AvisosSmtp(); // necessita servidor SMTP
}
// PROVABLE: les rep
public class GestorPrestecs {
public GestorPrestecs(Repositori repo, ServeiAvisos avisos) { ... }
}Amb la segona, a la prova passes un repositori en memòria i un servei d'avisos que només registra crides. Sense base de dades, sense xarxa, sense Spring, en un mil·lisegon.
- Interfícies a les fronteres
Tota dependència sobre alguna cosa externa —base de dades, xarxa, sistema de fitxers, rellotge, correu— ha d'estar darrere d'una interfície. Així la pots substituir a la prova. Aquestes interfícies són les fronteres del teu domini.
- Funcions pures
Una funció pura (mateixes entrades → mateixes sortides, sense efectes secundaris) és trivial de provar:
// Pura: es prova amb una linia
public static BigDecimal calcularMulta(long diesRetard, BigDecimal perDia, BigDecimal maxima) {
if (diesRetard <= 0) return BigDecimal.ZERO;
return perDia.multiply(BigDecimal.valueOf(diesRetard)).min(maxima);
}D'aquí un principi arquitectònic molt potent: separa el càlcul dels efectes. Fica tota la lògica de negoci en funcions pures i deixa els efectes (desar, enviar, registrar) en una capa prima al voltant. El que és pur es prova exhaustivament; el que és impur es prova amb dobles (11-06).
- L'estat es rep, no es busca
// MALAMENT: busca el seu estat en un lloc global
public BigDecimal calcular(Prestec p) {
BigDecimal perDia = ReglesNegoci.multaPerDia(); // static: no es pot substituir
LocalDate avui = LocalDate.now(); // rellotge del sistema
...
}
// BE: el rep
public BigDecimal calcular(Prestec p) {
BigDecimal perDia = propietats.multa().eurosPerDia(); // injectat
LocalDate avui = LocalDate.now(rellotge); // injectat
...
}
- Mètodes petits amb una responsabilitat
Un mètode de 200 línies amb vuit branques necessita desenes de proves per cobrir-se i cap no és llegible. Cinc mètodes de 15 línies es proven de cinc en cinc.
Resum:
| Propietat del disseny | Efecte en les proves | On ho vas aplicar |
|---|---|---|
| Injecció per constructor | Substituir col·laboradors | 11-02 |
| Interfícies a les fronteres | Simular BD, xarxa, correu | 10-03, 11-03 |
Clock injectat |
Controlar el temps | 10-05 |
| Funcions pures | Provar sense preparació | 10-04 (streams) |
| Sense estat global mutable | Proves independents | 11-02 (singletons sense estat) |
| Domini sense anotacions del framework | Provar sense arrencar Spring | 11-02 |
| Mètodes petits | Proves llegibles | Mòdul 3 |
- El
Clock injectable, per fi cobrat
Clock injectable, per fi cobratA 10-05 es va introduir el Clock amb aquesta justificació: "pensat precisament per poder provar-lo". Dos mòduls després, aquí hi ha la recompensa.
El problema sense Clock:
public BigDecimal calcular(Prestec prestec) {
LocalDate avui = LocalDate.now(); // el rellotge del SISTEMA
...
}Per provar que un préstec vençut fa 5 dies genera 2,50 € de multa, hauries de:
- Esperar cinc dies reals (absurd), o
- Crear el préstec amb data passada i confiar que el càlcul va bé (fràgil: la prova dóna resultats diferents segons el dia en què s'executi), o
- Canviar el rellotge del sistema operatiu (inacceptable).
I hi ha un problema pitjor: la prova passaria avui i fallaria l'1 de gener, o en canviar l'hora, o en un servidor en una altra zona horària. Això és una prova no determinista, el pitjor tipus.
La solució amb Clock:
public BigDecimal calcular(Prestec prestec) {
LocalDate avui = LocalDate.now(rellotge); // el rellotge INJECTAT
...
}class CalculadoraMultesTest {
// "Avui" es SEMPRE el 20 de marc de 2026. En qualsevol maquina, qualsevol dia.
private static final Clock RELLOTGE_FIX = Clock.fixed(
Instant.parse("2026-03-20T10:00:00Z"), ZoneId.of("Europe/Madrid"));
private CalculadoraMultes calculadora;
@BeforeEach
void preparar() {
calculadora = new CalculadoraMultes(propietatsDeProva(), RELLOTGE_FIX);
}
@Test
void unPrestecVencutFaCincDiesGeneraDosEurosCinquanta() {
Prestec prestec = prestecQueVaVencerEl(LocalDate.of(2026, 3, 15));
assertThat(calculadora.calcular(prestec)).isEqualByComparingTo("2.50");
}
@Test
void unPrestecQueVencAvuiNoGeneraMulta() {
Prestec prestec = prestecQueVaVencerEl(LocalDate.of(2026, 3, 20));
assertThat(calculadora.calcular(prestec)).isEqualByComparingTo("0.00");
}
@Test
void unPrestecQueVencDemaNoGeneraMulta() {
Prestec prestec = prestecQueVaVencerEl(LocalDate.of(2026, 3, 21));
assertThat(calculadora.calcular(prestec)).isEqualByComparingTo("0.00");
}
}I es pot viatjar en el temps dins d'una mateixa prova:
@Test
void laMultaCreixUnEuroMigPerCadaDiaQuePassa() {
LocalDate venciment = LocalDate.of(2026, 3, 15);
Prestec prestec = prestecQueVaVencerEl(venciment);
// Dia 16: 1 dia de retard
var dia16 = new CalculadoraMultes(propietats, rellotgeEn(LocalDate.of(2026, 3, 16)));
assertThat(dia16.calcular(prestec)).isEqualByComparingTo("0.50");
// Dia 20: 5 dies
var dia20 = new CalculadoraMultes(propietats, rellotgeEn(LocalDate.of(2026, 3, 20)));
assertThat(dia20.calcular(prestec)).isEqualByComparingTo("2.50");
// Dia 100: limitat al maxim
var dia100 = new CalculadoraMultes(propietats, rellotgeEn(LocalDate.of(2026, 6, 23)));
assertThat(dia100.calcular(prestec)).isEqualByComparingTo("20.00");
}
private static Clock rellotgeEn(LocalDate data) {
return Clock.fixed(data.atStartOfDay(ZoneId.of("Europe/Madrid")).toInstant(),
ZoneId.of("Europe/Madrid"));
}Tres dies diferents, tres resultats verificats, en cinc mil·lisegons. Això és el que valia aquella decisió de 10-05.
Altres rellotges útils de java.time:
| Fàbrica | Comportament |
|---|---|
Clock.fixed(instant, zona) |
Congelat en un instant |
Clock.systemDefaultZone() |
El del sistema (producció) |
Clock.offset(base, durada) |
Desplaçat: "d'aquí a 3 dies" |
Clock.tick(base, durada) |
Avança a salts (útil per reduir precisió) |
I la lliçó general, que va molt més enllà del rellotge:
Tot el que no controles —el temps, l'atzar, la xarxa, el sistema de fitxers, l'entorn— ha d'entrar per una dependència injectada. Si no, el teu codi no és determinista i no es pot provar.
El mateix val per a Random (injecta'n un amb llavor fixa), per a UUID.randomUUID() (injecta un generador) i per a les variables d'entorn (injecta la configuració).
- El codi que NO es pot provar
Reconèixer aquests patrons t'estalviarà hores.
static que llegeix estat global
// NO ES POT PROVAR
public class CalculadoraMultes {
public static BigDecimal calcular(Prestec p) {
BigDecimal perDia = ReglesNegoci.multaPerDia(); // static: no se substitueix
LocalDate avui = LocalDate.now(); // rellotge del sistema
...
}
}No hi ha cap punt d'inserció: no es pot substituir ReglesNegoci ni controlar el rellotge. És exactament el ReglesNegoci de 07-07, i per això 11-02 el va convertir en @ConfigurationProperties injectable.
new dins del mètode
// NO ES POT PROVAR SENSE XARXA
public class EnriquidorCataleg {
public Material enriquir(Material material) {
ClientMetadades client = new ClientMetadades(); // crida una API real
return client.enriquir(material);
}
}Cada execució de la prova faria una petició HTTP real: lenta, no determinista i dependent que l'API estigui disponible. La solució és injectar ClientMetadades darrere d'una interfície, i a 11-06 el simularàs.
Efectes secundaris ocults
public void processar(Prestec p) {
calcular(p);
enviarCorreu(p); // envia un correu DE VERITAT
System.out.println("Processat"); // no es pot comprovar
fitxerLog.append(...); // escriu en disc
}Cada prova enviaria un correu real. Separa el càlcul dels efectes.
Mètodes privats amb lògica complexa
Si sents la necessitat de provar un mètode privat, és senyal que aquesta lògica vol ser una classe. Extreu-la a una classe pròpia amb la seva interfície i prova-la directament. Fer servir reflexió per provar mètodes privats és un antipatró: acobla la prova a la implementació.
| Patró | Per què no es prova | Solució |
|---|---|---|
static amb estat global |
No hi ha on inserir el doble | Instància amb dependències injectades |
new al mètode |
No es pot substituir | Injectar per constructor |
LocalDate.now(), new Random() |
No determinista | Injectar Clock, Random |
System.out.println |
No es pot comprovar | Logger injectat o valor de retorn |
| Efectes barrejats amb càlcul | Cada prova causa efectes reals | Separar càlcul d'efectes |
| Mètodes privats complexos | Només accessibles per reflexió | Extreure a una altra classe |
| Constructor amb feina pesada | Construir ja causa efectes | Moure a un mètode d'inicialització |
- Què és un bon conjunt de proves
Les propietats que cal perseguir, amb les seves sigles habituals en anglès (FIRST):
| Propietat | Què significa | Com s'aconsegueix |
|---|---|---|
| Ràpid | Mil·lisegons per prova unitària | Sense BD, sense xarxa, sense sleep |
| Independent | Cada prova es pot executar sola | Sense estat compartit; @BeforeEach |
| Repetible | Mateix resultat sempre, en qualsevol màquina | Clock fix, sense dependències de l'entorn |
| Autoverificable | Verd o vermell, sense interpretació humana | Assercions, no System.out.println |
| Oportú | S'escriuen amb el codi, no un any després | Disciplina d'equip |
I dos criteris més que es fan servir molt:
- Independent de l'ordre: si en executar les proves en ordre aleatori alguna falla, hi ha estat compartit. JUnit permet forçar-ho amb
@TestMethodOrder(MethodOrderer.Random.class)per detectar-ho. - Sense lògica: zero
if,foritry/catchimprovisats. La prova ha de ser tan òbvia que no necessiti proves.
I la propietat més important de totes: una prova que mai no falla no val res. Comprova-ho trencant el codi a propòsit una vegada —canvia un <= per un <— i verifica que la prova es posa vermella. Si no, no està provant el que creus.
- Antipatrons
Proves fràgils. Es trenquen en refactoritzar encara que el comportament no canviï. Solen provar implementació en comptes de comportament:
// FRAGIL: comprova COM es fa
verify(repositori).findById(1L);
verify(repositori).save(any());
verify(calculadora).calcular(any());
// Si dema es fa servir findByIsbn, la prova falla encara que el resultat sigui identic.
// ROBUSTA: comprova QUIN resultat s'obte
assertThat(gestor.prestar("978-0000000001", "[email protected]"))
.extracting(Prestec::getDataVenciment)
.isEqualTo(LocalDate.of(2026, 3, 16));Aquest tema s'aprofundeix a 11-06, perquè l'excés de verify és el pecat capital de Mockito.
Proves que proven el framework. Comprovar que @Column(nullable = false) genera una columna NOT NULL, o que Spring injecta un bean, és provar codi de tercers que ja està provat. Prova la teva lògica.
Thread.sleep a les proves.
// MALAMENT
servei.processarAsincron();
Thread.sleep(1000); // i si triga 1100 ms a la integracio continua?
assertThat(resultat).isNotNull();És lent (multiplica-ho per cinquanta proves) i intermitent: passa al teu portàtil i falla al servidor d'integració. Les alternatives: CountDownLatch (08-05), CompletableFuture.get() amb timeout (08-07), o la llibreria Awaitility.
Proves intermitents (flaky). Fallen de vegades sense raó aparent. Causes típiques: dependència del rellotge, de l'ordre d'execució, de la concurrència o de la xarxa. Són pitjors que no tenir proves, perquè l'equip s'acostuma a ignorar les fallades. Una prova intermitent s'arregla o s'esborra el mateix dia.
Proves gegants. Cent línies de preparació i trenta assercions. Quan falla, ningú sap què s'ha trencat. Divideix.
Assercions buides o inútils.
@Test
void provaDePrestec() {
Prestec p = gestor.prestar("978-0000000001", "[email protected]");
assertNotNull(p); // i que? No comprova cap regla
}Copiar la implementació a la prova.
// Inutil: si la formula esta malament, la prova esta malament igual
BigDecimal esperat = perDia.multiply(BigDecimal.valueOf(dies)).min(maxima);
assertThat(calculadora.calcular(prestec)).isEqualByComparingTo(esperat);Els valors esperats s'escriuen a mà, calculats per una persona: "2.50". Aquest és el punt: la prova és una segona opinió independent.
- Provar codi concurrent
El mòdul 8 va deixar CatalegConcurrent, ExecutorService, CompletableFuture i col·leccions concurrents. Provar-los té una dificultat de fons:
Una prova de concurrència que passa no demostra que el codi sigui correcte. Només demostra que aquella vegada no es va manifestar la fallada.
Tot i així hi ha tècniques útils:
@Test
void elCatalegConcurrentSuportaCentFilsSimultanis() throws Exception {
CatalegConcurrent cataleg = new CatalegConcurrent();
int fils = 100, operacions = 1000;
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { // 10-06
CountDownLatch llestos = new CountDownLatch(fils);
CountDownLatch sortida = new CountDownLatch(1);
for (int i = 0; i < fils; i++) {
executor.submit(() -> {
llestos.countDown();
sortida.await(); // tots comencen ALHORA
for (int j = 0; j < operacions; j++) cataleg.incrementarConsultes();
return null;
});
}
llestos.await(5, TimeUnit.SECONDS);
sortida.countDown(); // tret simultani
} // el tancament de l'executor espera que acabin
assertThat(cataleg.getConsultes()).isEqualTo((long) fils * operacions);
}El truc dels dos CountDownLatch —un per esperar que tots estiguin llestos, un altre per disparar-los alhora— maximitza la probabilitat que la condició de cursa es manifesti. És el que feies a 08-05.
Recomanacions:
- Prefereix dissenys sense estat compartit. El codi concurrent que no comparteix estat no necessita proves de concurrència.
- Prova la lògica per separat, sense concurrència, i afegeix una prova d'estrès com a complement.
- Etiqueta-les com a lentes (
@Tag("lenta")) i treu-les del cicle ràpid. - Eines especialitzades (jcstress) existeixen per a verificació seriosa de concurrència; estan fora de l'abast d'aquest curs.
- Executar les proves amb Maven
Ordres que faràs servir a diari (11-05 les explica a fons):
mvn test # compila i executa les proves unitaries
mvn test -Dtest=CalculadoraMultesTest # nomes una classe
mvn test -Dtest=CalculadoraMultesTest#multa* # nomes metodes que comencin per "multa"
mvn test -Dgroups="rapida" # nomes les etiquetades
mvn verify # unitaries + integracio (failsafe)
mvn package -DskipTests # empaqueta SENSE executar provesUn avís sobre aquesta última: -DskipTests és útil per empaquetar ràpid en local. Mai a la integració contínua. Si el projecte necessita saltar-se proves per compilar, hi ha un problema més gran.
Quan alguna cosa falla, la sortida és:
[ERROR] Failures:
[ERROR] CalculadoraMultesTest.laMultaEsLimitaAlMaxim:47
expected: 20.00 but was: 100.00
[INFO] Tests run: 24, Failures: 1, Errors: 0, Skipped: 0
[INFO] BUILD FAILUREAmb l'informe complet a target/surefire-reports/. I l'important: la compilació falla. Això és el que impedeix que el codi trencat arribi a producció.
- Cobertura, esmentada
La cobertura de codi mesura quin percentatge del teu codi executen les proves. En Java es mesura amb JaCoCo.
Dos advertiments, i són seriosos:
- Cobertura no és qualitat. Una línia executada no és una línia provada. Aquest codi té 100 % de cobertura i no comprova res:
- Un objectiu de cobertura alt genera proves dolentes. Si l'equip ha d'assolir el 90 %, apareixeran proves de getters i setters que pugen el número sense aportar res.
El que sí que és útil: mirar què està sense cobrir. Un mètode de negoci amb 0 % de cobertura és informació valuosa.
Es desenvolupa a 12-05, amb la configuració de JaCoCo i la seva interpretació honesta.
@SpringBootTest i @DataJpaTest, presentats
@SpringBootTest i @DataJpaTest, presentatsFins aquí, tot han estat proves unitàries sense Spring, que és com ha de ser: ràpides i nombroses. Però també cal verificar que les peces encaixen.
@SpringBootTest arrenca el context complet:
@SpringBootTest
class BiblioTechApplicationTests {
@Autowired
private GestorPrestecs gestor;
@Test
void elContextEsCarregaCorrectament() {
assertThat(gestor).isNotNull();
}
}Aquesta prova, aparentment trivial, té valor real: detecta un bean que falta, una dependència circular o una propietat obligatòria sense definir. És la prova que evita descobrir al desplegament que l'aplicació no arrenca.
@DataJpaTest arrenca només la capa de persistència, amb una base de dades incrustada i transacció amb reversió automàtica:
@DataJpaTest
class PrestecRepositoryTest {
@Autowired private PrestecRepository repositori;
@Autowired private TestEntityManager em;
@Test
void trobaElsPrestecsVencutsSenseRetornar() {
Material llibre = em.persist(new Llibre("978-0000000001", "Java Eficac", 3, "J. Bloch"));
Empleat marta = em.persist(new Empleat("[email protected]", "Marta Ruiz"));
em.persist(new Prestec(llibre, marta,
LocalDate.of(2026, 3, 1), LocalDate.of(2026, 3, 10)));
em.flush();
List<Prestec> vencuts = repositori.vencuts(LocalDate.of(2026, 3, 20));
assertThat(vencuts).hasSize(1)
.first().extracting(p -> p.getMaterial().getTitol())
.isEqualTo("Java Eficac");
}
}Cada prova s'executa en una transacció que es reverteix en acabar, així que no es contaminen entre si.
| Anotació | Què arrenca | Velocitat |
|---|---|---|
| (cap) | Res. Java pur | 1-10 ms |
@DataJpaTest |
JPA, repositoris, BD incrustada | ~1 s |
@WebMvcTest |
Només la capa web (12-04) | ~1 s |
@SpringBootTest |
Tot el context | 2-10 s |
La regla: fes servir l'anotació més petita que serveixi. Cada prova amb @SpringBootTest que podria ser unitària és temps que pagues a cada compilació, per sempre.
Es desenvolupen a 11-06 (amb Mockito) i a 12-05.
- La bateria de proves de BiblioTech
El resultat complet. Estructura:
src/test/java/com/nexussoftware/bibliotech/
├── domini/
│ ├── PrestecTest.java <- regles de venciment i devolucio
│ ├── MaterialTest.java <- exemplars disponibles
│ └── IsbnTest.java <- validacio
├── servei/
│ ├── CalculadoraMultesTest.java <- multes, amb Clock fix
│ └── GestorPrestecsTest.java <- limit de prestecs
├── persistencia/
│ └── EscripturaAtomicaTest.java <- @TempDir
└── BiblioTechApplicationTests.java <- @SpringBootTest: carrega del contextCalculadoraMultesTest completa:
package com.nexussoftware.bibliotech.servei;
import static org.assertj.core.api.Assertions.*;
import java.math.BigDecimal;
import java.time.*;
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
@DisplayName("Càlcul de multes de BiblioTech")
class CalculadoraMultesTest {
private static final ZoneId MADRID = ZoneId.of("Europe/Madrid");
private static final LocalDate AVUI = LocalDate.of(2026, 3, 20);
private static final Clock RELLOTGE_FIX =
Clock.fixed(AVUI.atStartOfDay(MADRID).toInstant(), MADRID);
private CalculadoraMultes calculadora;
private Empleat marta;
private Material llibre;
@BeforeEach
void preparar() {
var propietats = new PropietatsBiblioTech(
"BiblioTech - Nexus Software",
new PropietatsBiblioTech.Prestec(15, 3),
new PropietatsBiblioTech.Multa(new BigDecimal("0.50"), new BigDecimal("20.00")),
new PropietatsBiblioTech.Reserva(3));
calculadora = new CalculadoraMultes(propietats, RELLOTGE_FIX);
marta = new Empleat("[email protected]", "Marta Ruiz");
llibre = new Llibre("978-0000000001", "Java Eficac", 3, "J. Bloch");
}
@Nested
@DisplayName("quan el préstec continua en termini")
class EnTermini {
@Test
@DisplayName("no hi ha multa si venç dins d'una setmana")
void senseMultaSiVencEnUnaSetmana() {
assertThat(calculadora.calcular(prestecQueVenc(AVUI.plusDays(7))))
.isEqualByComparingTo("0.00");
}
@Test
@DisplayName("no hi ha multa el mateix dia del venciment")
void senseMultaElDiaDelVenciment() {
assertThat(calculadora.calcular(prestecQueVenc(AVUI)))
.isEqualByComparingTo("0.00");
}
}
@Nested
@DisplayName("quan el préstec està vençut")
class Vencut {
@ParameterizedTest(name = "{0} dies de retard -> {1} EUR")
@CsvSource({
" 1, 0.50", " 2, 1.00", " 5, 2.50", " 10, 5.00",
" 39, 19.50", " 40, 20.00", " 41, 20.00", "200, 20.00"
})
@DisplayName("la multa és 0,50 € per dia fins al màxim de 20 €")
void multaPerDiesDeRetard(int dies, BigDecimal esperada) {
assertThat(calculadora.calcular(prestecQueVenc(AVUI.minusDays(dies))))
.isEqualByComparingTo(esperada);
}
@Test
@DisplayName("la multa mai no supera el màxim configurat")
void maiSuperaElMaxim() {
assertThat(calculadora.calcular(prestecQueVenc(AVUI.minusYears(2))))
.isEqualByComparingTo("20.00");
}
}
@Nested
@DisplayName("quan el préstec ja s'ha retornat")
class Retornat {
@Test
@DisplayName("no hi ha multa encara que es retornés tard")
void senseMultaDespresDeLaDevolucio() {
Prestec prestec = prestecQueVenc(AVUI.minusDays(30));
prestec.retornar(AVUI.minusDays(10));
assertThat(calculadora.calcular(prestec)).isEqualByComparingTo("0.00");
}
}
// --- fabriques d'escenaris ---
private Prestec prestecQueVenc(LocalDate venciment) {
return new Prestec(llibre, marta, venciment.minusDays(15), venciment);
}
}Execució:
[INFO] Running com.nexussoftware.bibliotech.servei.CalculadoraMultesTest
[INFO] Tests run: 12, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.089 s
[INFO] Running com.nexussoftware.bibliotech.domini.PrestecTest
[INFO] Tests run: 8, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.021 s
...
[INFO] Tests run: 41, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
[INFO] Total time: 6.412 sQuaranta-una proves en menys d'un segon d'execució efectiva. A partir d'ara, cada refactorització de BiblioTech té una xarxa a sota.
- Errors comuns i consells
Error: escriure proves que mai no fallen. Trenca el codi a propòsit i comprova que la prova es posa vermella. Si no, no prova res.
Error: provar la implementació en comptes del comportament. Si la prova es trenca en refactoritzar sense canviar el resultat, està mal escrita.
Error: Thread.sleep en proves. Lent i intermitent. CountDownLatch, Future.get(timeout) o Awaitility.
Error: dependència de l'ordre. Si les proves fallen en executar-se en ordre diferent, hi ha estat compartit. @BeforeEach, no camps estàtics mutables.
Error: LocalDate.now() al codi de producció. Fa la prova no determinista. Clock injectat.
Error: assertEquals amb BigDecimal. new BigDecimal("2.50").equals(new BigDecimal("2.5")) és false. Fes servir isEqualByComparingTo d'AssertJ o compareTo.
Error: invertir l'ordre a assertEquals. L'esperat va primer; al revés, els missatges enganyen.
Error: try/catch en comptes d'assertThrows. Si no es llança l'excepció, la prova passa igualment.
Error: @Disabled sense motiu. I una prova desactivada més d'un mes cal esborrar-la: està donant falsa sensació de cobertura.
Error: @SpringBootTest per a tot. Multiplica el temps del conjunt per mil sense aportar res quan la classe es pot construir amb new.
Error: perseguir un percentatge de cobertura. Genera proves de getters que no verifiquen res.
Consell: fes servir AssertJ. Ja el tens, i els seus missatges d'error estalvien temps real.
Consell: noms llargs i descriptius. laMultaEsLimitaAlMaximConfigurat és millor que testMulta. Ningú crida aquests mètodes: el seu únic propòsit és comunicar.
Consell: una prova, un comportament. Si el nom necessita un "i", són dues proves.
Consell: proves parametritzades per a casos límit. El cost d'afegir el cas 39, 40 i 41 és una línia, i just allà hi ha els bugs.
Consell: escriu primer la prova que falla. Encara que no practiquis TDD estricte, començar per una prova vermella garanteix que la prova prova alguna cosa.
Consell: quan aparegui un bug, escriu primer la prova que el reprodueix. Arregla'l després. Així aquest bug concret no torna mai.
Consell: si alguna cosa és difícil de provar, el problema és al disseny. No et barallis amb la prova: arregla el disseny.
- Exercicis
Exercici 1: provar les regles de Prestec
Aquesta és l'entitat Prestec de BiblioTech després d'11-03:
@Entity
public class Prestec {
@Id @GeneratedValue private Long id;
@ManyToOne(fetch = FetchType.LAZY) private Material material;
@ManyToOne(fetch = FetchType.LAZY) private Empleat empleat;
private LocalDate dataPrestec;
private LocalDate dataVenciment;
private LocalDate dataDevolucio;
@Enumerated(EnumType.STRING) private EstatPrestec estat;
public Prestec(Material material, Empleat empleat, LocalDate dataPrestec, int dies) {
this.material = material;
this.empleat = empleat;
this.dataPrestec = dataPrestec;
this.dataVenciment = dataPrestec.plusDays(dies);
this.estat = EstatPrestec.ACTIU;
}
public boolean estaVencut(LocalDate avui) {
return dataDevolucio == null && avui.isAfter(dataVenciment);
}
public void retornar(LocalDate data) {
if (dataDevolucio != null) {
throw new PrestecJaRetornatException(id);
}
if (data.isBefore(dataPrestec)) {
throw new DataInvalidaException("No es pot retornar abans de prestar");
}
this.dataDevolucio = data;
this.estat = EstatPrestec.RETORNAT;
}
public Optional<LocalDate> getDataDevolucio() {
return Optional.ofNullable(dataDevolucio);
}
}Escriu una classe de prova completa que cobreixi:
- Que un préstec nou té les dades correctes (fes servir
assertAll). estaVencutamb almenys cinc casos, inclosos els límits (el dia del venciment i el dia següent), mitjançant una prova parametritzada.- Que retornar funciona i canvia l'estat.
- Que retornar dues vegades llança
PrestecJaRetornatException. - Que retornar abans de prestar llança
DataInvalidaException. - Que un préstec retornat mai no està vençut, encara que es retornés tard.
- Organitza amb
@Nestedi@DisplayName.
Exercici 2: fer provable el que no ho és
Aquesta classe de BiblioTech no es pot provar. Identifica tots els problemes, reescriu-la i escriu tres proves que abans eren impossibles.
public class ServeiAvisosVenciment {
public int enviarAvisosDelDia() {
int enviats = 0;
LocalDate avui = LocalDate.now();
RepositoriPrestecs repositori = new RepositoriPrestecsJpa();
List<Prestec> prestecs = repositori.cercarTots();
for (Prestec p : prestecs) {
long dies = ChronoUnit.DAYS.between(avui, p.getDataVenciment());
if (dies == Integer.parseInt(ReglesNegoci.get("avis.dies.antelacio", "2"))) {
ClientCorreu correu = new ClientCorreu("smtp.nexussoftware.com", 587);
correu.enviar(p.getEmpleat().getCorreu(),
"El teu prestec de " + p.getMaterial().getTitol() + " venc aviat");
System.out.println("Avis enviat a " + p.getEmpleat().getCorreu());
enviats++;
}
}
return enviats;
}
}Es demana:
- Llista de tots els obstacles per provar-la.
- Versió reescrita, provable, sense canviar el comportament.
- Tres proves: s'avisa quan falten exactament els dies configurats; no s'avisa quan en falten més o menys; es compten correctament els avisos enviats.
(Nota: aquí faràs servir implementacions falses en memòria de les interfícies. Els simulats amb Mockito són 11-06.)
Exercici 3: trobar el bug amb una prova
Un company de Nexus Software ha implementat la renovació de préstecs. Marta Ruiz informa que "de vegades renovar no afegeix dies". El codi:
public class GestorRenovacions {
private static final int MAX_RENOVACIONS = 2;
private final Clock rellotge;
public GestorRenovacions(Clock rellotge) { this.rellotge = rellotge; }
public LocalDate renovar(Prestec prestec, int dies) {
if (prestec.getRenovacions() >= MAX_RENOVACIONS) {
throw new LimitRenovacionsException(prestec.getId());
}
if (prestec.getDataDevolucio().isPresent()) {
throw new PrestecJaRetornatException(prestec.getId());
}
LocalDate avui = LocalDate.now(rellotge);
LocalDate novaData = prestec.getDataVenciment().plusDays(dies);
if (novaData.isBefore(avui)) {
novaData = avui;
}
prestec.actualitzarVenciment(novaData);
prestec.incrementarRenovacions();
return novaData;
}
}Es demana:
- Escriu proves que cobreixin el cas normal, el límit de renovacions i el préstec ja retornat.
- Troba el bug escrivint la prova que el revela. Pista: pensa en un préstec que porta molt de temps vençut.
- Explica el comportament incorrecte des del punt de vista del negoci.
- Corregeix el codi i demostra que la prova passa.
Solucions
Solució 1
package com.nexussoftware.bibliotech.domini;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertAll;
import java.time.LocalDate;
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
@DisplayName("Préstec")
class PrestecTest {
private static final LocalDate DATA_PRESTEC = LocalDate.of(2026, 3, 1);
private static final int DIES = 15;
private static final LocalDate VENCIMENT = LocalDate.of(2026, 3, 16);
private Material llibre;
private Empleat marta;
private Prestec prestec;
@BeforeEach
void preparar() {
llibre = new Llibre("978-0000000001", "Java Eficac", 3, "J. Bloch");
marta = new Empleat("[email protected]", "Marta Ruiz");
prestec = new Prestec(llibre, marta, DATA_PRESTEC, DIES);
}
@Nested
@DisplayName("acabat de crear")
class AcabatDeCrear {
@Test
@DisplayName("(1) té totes les dades correctes")
void dadesCorrectes() {
assertAll("dades del prestec",
() -> assertThat(prestec.getMaterial()).isEqualTo(llibre),
() -> assertThat(prestec.getEmpleat()).isEqualTo(marta),
() -> assertThat(prestec.getDataPrestec()).isEqualTo(DATA_PRESTEC),
() -> assertThat(prestec.getDataVenciment()).isEqualTo(VENCIMENT),
() -> assertThat(prestec.getEstat()).isEqualTo(EstatPrestec.ACTIU),
() -> assertThat(prestec.getDataDevolucio()).isEmpty());
}
}
@Nested
@DisplayName("comprovació de venciment")
class Venciment {
// (2) cinc casos, inclosos els limits
@ParameterizedTest(name = "el {0} -> vencut = {1}")
@CsvSource({
"2026-03-01, false", // el dia del prestec
"2026-03-15, false", // vigilia del venciment
"2026-03-16, false", // LIMIT: el mateix dia NO esta vencut
"2026-03-17, true", // LIMIT: el dia seguent SI
"2026-04-30, true", // molt despres
"2027-01-01, true"
})
void estaVencutSegonsLaData(LocalDate avui, boolean esperat) {
assertThat(prestec.estaVencut(avui)).isEqualTo(esperat);
}
}
@Nested
@DisplayName("en retornar")
class Devolucio {
@Test
@DisplayName("(3) registra la data i canvia l'estat")
void devolucioCorrecta() {
LocalDate dataDevolucio = LocalDate.of(2026, 3, 10);
prestec.retornar(dataDevolucio);
assertAll(
() -> assertThat(prestec.getDataDevolucio()).contains(dataDevolucio),
() -> assertThat(prestec.getEstat()).isEqualTo(EstatPrestec.RETORNAT));
}
@Test
@DisplayName("(4) retornar dues vegades llança PrestecJaRetornatException")
void noEsPotRetornarDuesVegades() {
prestec.retornar(LocalDate.of(2026, 3, 10));
assertThatThrownBy(() -> prestec.retornar(LocalDate.of(2026, 3, 11)))
.isInstanceOf(PrestecJaRetornatException.class);
}
@Test
@DisplayName("(5) retornar abans de prestar llança DataInvalidaException")
void noEsPotRetornarAbansDePrestar() {
assertThatThrownBy(() -> prestec.retornar(DATA_PRESTEC.minusDays(1)))
.isInstanceOf(DataInvalidaException.class)
.hasMessageContaining("abans de prestar");
}
@Test
@DisplayName("(6) un préstec retornat mai no està vençut, encara que es retornés tard")
void retornatMaiEstaVencut() {
prestec.retornar(LocalDate.of(2026, 4, 30)); // 45 dies tard
assertThat(prestec.estaVencut(LocalDate.of(2027, 1, 1))).isFalse();
}
}
}Notes sobre les decisions:
- Els casos límit del punt 2 són els que importen: el dia 16 (venç, però no està vençut) i el 17 (sí que ho està). Allà és on viu el bug de
>enfront de>=, i una prova parametritzada els cobreix per una línia cadascun. - El punt 6 prova una regla de negoci real, no una línia de codi: un préstec tancat no pot estar vençut, encara que la seva data de devolució sigui posterior al venciment.
assertThat(optional).contains(x)d'AssertJ és més net queisPresent()seguit deget().- El
@BeforeEachdeixa l'escenari en un estat conegut; cada prova parteix de zero.
Solució 2
1. Obstacles per provar la versió original.
| Línia | Problema | Conseqüència |
|---|---|---|
LocalDate.now() |
Rellotge del sistema | La prova dóna resultats diferents segons el dia |
new RepositoriPrestecsJpa() |
Instancia una dependència real | Necessita base de dades |
ReglesNegoci.get(...) |
Estàtic i global | No es pot configurar a la prova |
Integer.parseInt al bucle |
Conversió repetida | Menor, però és lògica repetida |
new ClientCorreu(...) al bucle |
Crea un client SMTP per avís | Enviaria correus reals; a més, ineficient |
System.out.println |
Efecte no comprovable | No es pot verificar |
| Sense abstracció del correu | Acoblat a SMTP | No hi ha on posar un doble |
2. Versió provable.
package com.nexussoftware.bibliotech.servei;
import java.time.Clock;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.List;
import org.slf4j.*;
import org.springframework.stereotype.Service;
@Service
public class ServeiAvisosVenciment {
private static final Logger log = LoggerFactory.getLogger(ServeiAvisosVenciment.class);
private final RepositoriPrestecs repositori; // interficie
private final ServeiAvisos avisos; // interficie
private final PropietatsBiblioTech propietats; // configuracio injectada
private final Clock rellotge; // temps controlable
public ServeiAvisosVenciment(RepositoriPrestecs repositori,
ServeiAvisos avisos,
PropietatsBiblioTech propietats,
Clock rellotge) {
this.repositori = repositori;
this.avisos = avisos;
this.propietats = propietats;
this.rellotge = rellotge;
}
public int enviarAvisosDelDia() {
LocalDate avui = LocalDate.now(rellotge);
int diesAntelacio = propietats.avis().diesAntelacio();
List<Prestec> aAvisar = repositori.cercarActius().stream()
.filter(p -> ChronoUnit.DAYS.between(avui, p.getDataVenciment()) == diesAntelacio)
.toList();
aAvisar.forEach(p -> {
avisos.avisar(p.getEmpleat(),
"El teu prestec de " + p.getMaterial().getTitol() + " venc aviat");
log.info("Avis enviat a {}", p.getEmpleat().getCorreu());
});
return aAvisar.size();
}
}Canvis i el seu motiu:
| Abans | Ara | Per què |
|---|---|---|
LocalDate.now() |
LocalDate.now(rellotge) |
Determinista |
new RepositoriPrestecsJpa() |
Interfície injectada | Substituïble per un fals en memòria |
ReglesNegoci.get(...) |
propietats.avis().diesAntelacio() |
Configurable i validat |
new ClientCorreu(...) al bucle |
ServeiAvisos injectat |
Substituïble, i una sola instància |
System.out.println |
Logger (11-07) | Configurable per entorn |
cercarTots() |
cercarActius() |
Menys dades: el filtratge a la consulta |
3. Les tres proves.
package com.nexussoftware.bibliotech.servei;
import static org.assertj.core.api.Assertions.assertThat;
import java.time.*;
import java.util.*;
import org.junit.jupiter.api.*;
@DisplayName("Avisos de venciment")
class ServeiAvisosVencimentTest {
private static final ZoneId MADRID = ZoneId.of("Europe/Madrid");
private static final LocalDate AVUI = LocalDate.of(2026, 3, 20);
private static final Clock RELLOTGE = Clock.fixed(AVUI.atStartOfDay(MADRID).toInstant(), MADRID);
// Doble de prova tipus FAKE: implementacio real, pero en memoria
static class RepositoriEnMemoria implements RepositoriPrestecs {
private final List<Prestec> prestecs = new ArrayList<>();
void afegir(Prestec p) { prestecs.add(p); }
@Override public List<Prestec> cercarActius() { return List.copyOf(prestecs); }
}
// Doble tipus SPY casola: registra el que se li demana
static class AvisosRegistrats implements ServeiAvisos {
final List<String> destinataris = new ArrayList<>();
final List<String> missatges = new ArrayList<>();
@Override public void avisar(Empleat destinatari, String missatge) {
destinataris.add(destinatari.getCorreu());
missatges.add(missatge);
}
}
private RepositoriEnMemoria repositori;
private AvisosRegistrats avisos;
private ServeiAvisosVenciment servei;
@BeforeEach
void preparar() {
repositori = new RepositoriEnMemoria();
avisos = new AvisosRegistrats();
var propietats = propietatsAmbAntelacioDe(2); // avisar 2 dies abans
servei = new ServeiAvisosVenciment(repositori, avisos, propietats, RELLOTGE);
}
@Test
@DisplayName("avisa quan falten exactament els dies configurats")
void avisaAmbLAntelacioConfigurada() {
repositori.afegir(prestecQueVenc(AVUI.plusDays(2), "[email protected]"));
int enviats = servei.enviarAvisosDelDia();
assertThat(enviats).isEqualTo(1);
assertThat(avisos.destinataris).containsExactly("[email protected]");
assertThat(avisos.missatges).first().asString().contains("Java Eficac");
}
@Test
@DisplayName("no avisa si falten més o menys dies dels configurats")
void noAvisaForaDeLaFinestra() {
repositori.afegir(prestecQueVenc(AVUI.plusDays(1), "[email protected]")); // 1 dia
repositori.afegir(prestecQueVenc(AVUI.plusDays(3), "[email protected]")); // 3 dies
repositori.afegir(prestecQueVenc(AVUI, "[email protected]")); // avui
repositori.afegir(prestecQueVenc(AVUI.minusDays(5),"[email protected]")); // vencut
int enviats = servei.enviarAvisosDelDia();
assertThat(enviats).isZero();
assertThat(avisos.destinataris).isEmpty();
}
@Test
@DisplayName("compta correctament els avisos enviats")
void comptaElsAvisos() {
repositori.afegir(prestecQueVenc(AVUI.plusDays(2), "[email protected]"));
repositori.afegir(prestecQueVenc(AVUI.plusDays(2), "[email protected]"));
repositori.afegir(prestecQueVenc(AVUI.plusDays(2), "[email protected]"));
repositori.afegir(prestecQueVenc(AVUI.plusDays(9), "[email protected]"));
assertThat(servei.enviarAvisosDelDia()).isEqualTo(3);
assertThat(avisos.destinataris).containsExactlyInAnyOrder(
"[email protected]",
"[email protected]",
"[email protected]");
}
private Prestec prestecQueVenc(LocalDate venciment, String correu) {
return new Prestec(
new Llibre("978-0000000001", "Java Eficac", 3, "J. Bloch"),
new Empleat(correu, "Empleat de prova"),
venciment.minusDays(15), venciment);
}
}Observa la segona prova: verifica els quatre casos límit al voltant de la finestra d'avís (1 dia, 3 dies, avui, vençut). Aquest == diesAntelacio és exactament on un >= mal posat avisaria tothom cada dia, i aquesta prova ho detectaria.
I observa els dos dobles casolans: un fake (repositori en memòria amb comportament real) i un spy (registra el que rep). A 11-06 veuràs els seus noms precisos i com Mockito els genera sol.
Solució 3
1 i 2. Les proves, inclosa la que revela el bug.
package com.nexussoftware.bibliotech.servei;
import static org.assertj.core.api.Assertions.*;
import java.time.*;
import org.junit.jupiter.api.*;
@DisplayName("Renovació de préstecs")
class GestorRenovacionsTest {
private static final ZoneId MADRID = ZoneId.of("Europe/Madrid");
private static final LocalDate AVUI = LocalDate.of(2026, 3, 20);
private static final Clock RELLOTGE = Clock.fixed(AVUI.atStartOfDay(MADRID).toInstant(), MADRID);
private GestorRenovacions gestor;
@BeforeEach
void preparar() { gestor = new GestorRenovacions(RELLOTGE); }
@Test
@DisplayName("renovar afegeix els dies a la data de venciment")
void renovarAfegeixDies() {
Prestec prestec = prestecQueVenc(AVUI.plusDays(3)); // venc el 23
LocalDate nova = gestor.renovar(prestec, 15);
assertThat(nova).isEqualTo(LocalDate.of(2026, 4, 7)); // 23 + 15
assertThat(prestec.getRenovacions()).isEqualTo(1);
}
@Test
@DisplayName("no es pot renovar més de dues vegades")
void limitDeRenovacions() {
Prestec prestec = prestecQueVenc(AVUI.plusDays(3));
gestor.renovar(prestec, 15);
gestor.renovar(prestec, 15);
assertThatThrownBy(() -> gestor.renovar(prestec, 15))
.isInstanceOf(LimitRenovacionsException.class);
}
@Test
@DisplayName("no es pot renovar un préstec ja retornat")
void noEsRenovaUnRetornat() {
Prestec prestec = prestecQueVenc(AVUI.plusDays(3));
prestec.retornar(AVUI.minusDays(1));
assertThatThrownBy(() -> gestor.renovar(prestec, 15))
.isInstanceOf(PrestecJaRetornatException.class);
}
// ===== LA PROVA QUE REVELA EL BUG =====
@Test
@DisplayName("renovar un préstec molt vençut ha d'afegir els dies des d'AVUI, no des del passat")
void renovarUnPrestecMoltVencut() {
// Marta tenia un prestec que va vencer fa 60 dies (el 19 de gener)
Prestec prestec = prestecQueVenc(AVUI.minusDays(60));
LocalDate nova = gestor.renovar(prestec, 15);
// Esperat per negoci: avui (20 de marc) + 15 dies = 4 d'abril
assertThat(nova).isEqualTo(LocalDate.of(2026, 4, 4));
}
}Aquesta última prova falla:
3. El comportament incorrecte.
Traçant el codi amb un préstec que va vèncer el 19 de gener de 2026 i una renovació de 15 dies:
LocalDate avui = LocalDate.of(2026, 3, 20);
LocalDate novaData = LocalDate.of(2026, 1, 19).plusDays(15); // = 2026-02-03
if (novaData.isBefore(avui)) { // 3 de febrer es anterior al 20 de marc -> true
novaData = avui; // es queda a AVUI!
}El préstec es renova amb venciment avui mateix: Marta prem "Renovar 15 dies" i el seu préstec venç aquella mateixa tarda. I pitjor: ha gastat una de les seves dues renovacions a canvi de res. L'endemà torna a estar vençut, amb multa, i només li queda una renovació.
Aquest és el "de vegades renovar no afegeix dies" de l'informe. És "de vegades" perquè només passa quan el retard acumulat supera els dies de renovació —just el cas de qui més necessita renovar—.
L'arrel de l'error és que la clàusula de seguretat if (novaData.isBefore(avui)) tenia una bona intenció —evitar que una renovació deixi el venciment al passat— però va triar malament la correcció: en comptes de recalcular des d'avui, es va limitar a avui.
4. La correcció.
public LocalDate renovar(Prestec prestec, int dies) {
if (prestec.getRenovacions() >= MAX_RENOVACIONS) {
throw new LimitRenovacionsException(prestec.getId());
}
if (prestec.getDataDevolucio().isPresent()) {
throw new PrestecJaRetornatException(prestec.getId());
}
LocalDate avui = LocalDate.now(rellotge);
// La renovacio sempre compta des de la data MES TARDANA entre avui i el
// venciment actual. Aixi es garanteix que SEMPRE s'afegeixen `dies` complets
// de prestec util, tant si el prestec esta en termini com si esta vencut.
LocalDate base = prestec.getDataVenciment().isAfter(avui)
? prestec.getDataVenciment()
: avui;
LocalDate novaData = base.plusDays(dies);
prestec.actualitzarVenciment(novaData);
prestec.incrementarRenovacions();
return novaData;
}Comprovació amb les quatre proves:
| Escenari | Venciment actual | Base | Resultat |
|---|---|---|---|
| En termini (venç el 23) | 2026-03-23 | 2026-03-23 | 2026-04-07 ✔ |
| Venç avui | 2026-03-20 | 2026-03-20 | 2026-04-04 ✔ |
| Vençut fa 60 dies | 2026-01-19 | 2026-03-20 (avui) | 2026-04-04 ✔ |
Les quatre proves passen, i la que revelava el bug es queda al conjunt per sempre: si algú reintrodueix la lògica antiga durant una refactorització, es posarà vermella a l'instant.
Aquesta és la pràctica professional del consell de l'apartat 32: davant d'un bug, primer la prova que el reprodueix, després l'arranjament. Així aquest bug concret no torna mai.
Conclusió
BiblioTech té per fi una xarxa de seguretat.
Entens el cost de no tenir proves, que portaves onze lliçons pagant sense saber-ho: por de canviar, regressions descobertes en producció, depuració lenta, verificació manual repetida i —el que connecta amb Log4Shell d'11-01— la incapacitat d'actualitzar una dependència amb rapidesa quan apareix una vulnerabilitat. I saps què et donen a canvi: refactoritzar sense por, documentació executable que no es pot desactualitzar perquè si menteix es posa vermella, i millor disseny, perquè les proves són un detector de problemes estructurals.
Coneixes la piràmide de proves i per què la seva forma importa: moltes unitàries que triguen mil·lisegons, algunes d'integració, molt poques d'extrem a extrem. Un conjunt invertit triga vint minuts i ningú l'executa.
Domines JUnit 5: la seva arquitectura de Platform, Jupiter i Vintage; les dependències que porta spring-boot-starter-test; l'anatomia Preparar-Actuar-Comprovar amb les seves regles —una acció per prova, sense lògica, un comportament per prova—; noms llargs i descriptius amb @DisplayName; les assercions de JUnit amb assertThrows que retorna l'excepció per inspeccionar-la i assertAll que reporta totes les fallades d'una vegada; el cicle de vida amb @BeforeEach preferit sobre @BeforeAll; la instància nova per prova que garanteix independència; @Disabled sempre amb motiu i les anotacions condicionals; @Nested per organitzar per escenari amb preparació pròpia; @Tag per a l'execució selectiva; i @TempDir, que per fi va permetre provar el codi de fitxers del mòdul 7 —inclosa la propietat que va motivar escriure EscripturaAtomica: que una fallada a mitges no corrompi el fitxer existent—.
I domines les proves parametritzades, que van convertir dotze casos del càlcul de multes —amb els tres límits que de veritat importen, 39, 40 i 41 al voltant del màxim— en sis línies. Amb @CsvSource, @EnumSource, @MethodSource per a objectes complexos i el patró canònic de @NullAndEmptySource més @ValueSource per provar validació d'entrada.
Saps per què es recomana AssertJ: no per estètica, sinó perquè hasSize(3) et diu què hi havia quan falla, perquè isEqualByComparingTo resol el problema real que new BigDecimal("2.50").equals(new BigDecimal("2.5")) sigui false, i perquè el seu encadenament sobre col·leccions i Optional expressa comprovacions que amb JUnit serien bucles.
I sobretot entens què fa provable un disseny, que és la part que canvia com escrius codi de producció: injecció per constructor, interfícies a les fronteres, funcions pures separades dels efectes, estat que es rep en comptes de buscar-se, i mètodes petits. Amb el contrapunt: reconeixes el codi que no es pot provar —static que llegeix estat global, new dins del mètode, LocalDate.now(), efectes ocults, mètodes privats amb lògica complexa— i saps que quan alguna cosa és difícil de provar, el problema és al disseny, no a la prova.
I el Clock injectable de 10-05 s'ha cobrat per fi. Amb Clock.fixed, "avui" és sempre el 20 de març de 2026, en qualsevol màquina i qualsevol dia de l'any; es pot viatjar en el temps dins d'una mateixa prova i verificar tres dates diferents en cinc mil·lisegons. Amb la lliçó general que va molt més enllà del rellotge: tot el que no controles —temps, atzar, xarxa, fitxers, entorn— ha d'entrar per una dependència injectada.
Coneixes les propietats d'un bon conjunt —ràpid, independent, repetible, autoverificable, sense lògica, independent de l'ordre— i els seus antipatrons: proves fràgils que proven implementació, proves que proven el framework, Thread.sleep, proves intermitents que són pitjors que no tenir proves, i copiar la fórmula de la implementació al valor esperat.
Saps executar-les amb Maven, interpretar l'informe i —el decisiu— que la compilació falla quan una prova es trenca. Coneixes la cobertura i la seva interpretació honesta, remesa a 12-05. I tens presentats @SpringBootTest i @DataJpaTest, amb la regla d'or: fes servir l'anotació més petita que serveixi.
Quaranta-una proves en menys d'un segon. I ara, la pregunta incòmoda d'aquesta lliçó.
Què és exactament mvn test?
L'has escrit cinc vegades. Has vist que compila, que executa les proves, que escriu informes a target/surefire-reports/ i que fa fallar la compilació si alguna cosa es posa vermella. Has posat <scope>test</scope> en una dependència i has donat per bo que això fa que AssertJ i JUnit no acabin dins del jar de producció. Has donat per fet que src/test/java és on van les proves i que el projecte les sap trobar. Has vist aparèixer maven-surefire-plugin en un pom.xml sense saber què és un plugin. I a 11-01 es va parlar de mvn dependency:tree i de coordenades groupId:artifactId:version com si ja sabessis què són.
Res d'això s'ha explicat. I tanmateix porta tres lliçons sostenint tot el que has fet: les dependències de Spring, les d'Hibernate, les d'H2, les de JUnit, el jar executable, el spring-boot:run.
Hi ha una cosa més greu. Fins al mòdul 10, BiblioTech es compilava amb javac a mà i un classpath escrit a mà. Amb quaranta jars a l'arbre de dependències, això ja no és tediós: és senzillament impossible. I una de les lliçons de Log4Shell (11-01) era que la capacitat de respondre a una vulnerabilitat depèn de poder canviar una versió amb una línia i verificar amb una ordre que res no s'ha trencat. Acabes d'aconseguir la segona meitat d'aquesta frase. Falta la primera.
A la propera lliçó s'explica l'eina que ha estat a sota tot el temps. Veuràs quin problema resol una eina de construcció enfront del javac del mòdul 1; la convenció sobre configuració que explica per què src/test/java funciona sense configurar res; el POM complet i comentat de BiblioTech; les dependències amb els seus àmbits —inclòs aquell test que ja vas fer servir—, les seves dependències transitives i la regla que resol els conflictes de versions; el cicle de vida amb les seves fases, i per què mvn test compila abans de provar sense que li ho demanis; els plugins, entre ells el surefire que executa les teves proves i el failsafe que executarà les d'integració; els perfils, els projectes multimòdul, el Maven Wrapper i la comparació honesta amb Gradle.
I en acabar, BiblioTech serà un projecte Maven complet i reproduïble, amb totes les seves dependències declarades, executable amb una sola ordre a qualsevol màquina del món que tingui Java 17.
Després d'això tornarem a les proves — perquè les que has escrit avui només han pogut cobrir el que no té col·laboradors de veritat, i això s'ha de resoldre.
Curs de Programació en Java
Mòdul 1: Introducció a Java
- Introducció a Java
- Configuració de l'entorn de desenvolupament
- Sintaxi i estructura bàsica
- Variables i tipus de dades
- Operadors
- Entrada i sortida per consola
- El teu primer programa complet: BiblioTech
Mòdul 2: Flux de control
- Sentències condicionals
- Bucles
- Sentències switch
- Break i continue
- Depuració i traces d'execució
- Projecte: menú interactiu de BiblioTech
Mòdul 3: Programació orientada a objectes
- Introducció a la POO
- Classes i objectes
- Mètodes
- Constructors
- Herència
- Polimorfisme
- Encapsulament
- Abstracció
- La classe Object: equals, hashCode i toString
Mòdul 4: Programació orientada a objectes avançada
- Interfícies
- Classes abstractes
- Classes internes
- Classes anònimes
- Expressions lambda
- Interfícies funcionals i referències a mètodes
- Enumeracions i registres
Mòdul 5: Estructures de dades i col·leccions
- Arrays
- El framework de col·leccions
- ArrayList
- LinkedList
- HashMap
- HashSet
- Cua i Deque
- Pila
- Ordenació i cerca en col·leccions
Mòdul 6: Gestió d'excepcions
- Introducció a les excepcions
- Bloc try-catch
- Throw i throws
- Excepcions personalitzades
- Bloc finally
- Try-with-resources i AutoCloseable
- Estratègies de gestió d'errors i logging
Mòdul 7: Entrada/sortida de fitxers
- Lectura de fitxers
- Escriptura de fitxers
- Fluxos de fitxers
- BufferedReader i BufferedWriter
- Serialització
- L'API NIO.2: Path i Files
- Formats d'intercanvi: CSV i Properties
Mòdul 8: Multifil i concurrència
- Introducció al multifil
- Creació de fils
- Cicle de vida d'un fil
- Sincronització
- Utilitats de concurrència
- Col·leccions concurrents i variables atòmiques
- Tasques asíncrones amb CompletableFuture
Mòdul 9: Xarxes
- Introducció a les xarxes
- Sockets
- ServerSocket
- DatagramSocket i DatagramPacket
- URL i HttpURLConnection
- El client HTTP modern
Mòdul 10: Temes avançats
- Genèrics
- Anotacions
- Reflexió
- Característiques de Java 8: Streams i Optional
- Dates i hores amb java.time
- Java 9 i més enllà
- Memòria, recol·lecció de brossa i rendiment
Mòdul 11: Frameworks i llibreries de Java
- Introducció als frameworks de Java
- Spring Framework
- Hibernate
- JUnit
- Maven
- Proves avançades amb Mockito
- Llibreries essencials de l'ecosistema
