Les dues últimes lliçons van deixar BiblioTech amb quaranta-una proves i un projecte Maven reproduïble. I també van deixar un límit molt clar.
Tot el que s'ha provat fins ara era fàcil de provar: CalculadoraMultes, que només necessita un Clock fix; les regles de Prestec, que són Java pur; EscripturaAtomica, amb un directori temporal. Tan bon punt va aparèixer un col·laborador de veritat, va caldre escriure a mà una implementació en memòria del repositori i una altra que registrés les crides al servei d'avisos. Vint línies de classes internes per prova, amb dos col·laboradors senzills.
GestorPrestecs en té cinc. PrestecRepository és una interfície de Spring Data amb vint mètodes heretats: implementar-la a mà és senzillament inviable. I el ClientMetadades de 09-06 necessita una API HTTP real a l'altre costat.
A més hi ha preguntes que les assercions sobre el resultat no responen. Es va enviar l'avís a l'empleat correcte, amb el text correcte? Es va cridar el repositori una vegada o quaranta? Què passa quan la base de dades llança una excepció, un escenari dificilíssim de provocar amb una implementació real i que en producció passarà un dimarts qualsevol?
Mockito respon a tot això. És la llibreria de dobles de prova estàndard en Java: genera implementacions falses d'interfícies i classes en temps d'execució —amb la generació de bytecode que vas estudiar a 10-03—, permet programar el seu comportament i verificar com s'han fet servir.
Però aquesta lliçó no va només d'una API. Va del criteri per fer-la servir: què simular i què no, quan verificar interaccions i quan comprovar només el resultat, i per què l'excés de simulació produeix exactament aquelles proves fràgils que 11-04 va posar entre els antipatrons. Perquè una prova mal feta amb Mockito pot ser pitjor que cap prova.
En acabar sabràs distingir els cinc tipus de doble; dominaràs l'API de Mockito 5; verificaràs interaccions amb criteri; capturaràs arguments; sabràs què no s'ha de simular mai; i coneixeràs les proves d'integració amb @SpringBootTest i @DataJpaTest.
Contingut
- Per què no n'hi ha prou amb JUnit
- Els cinc tipus de doble de prova
- El fake en memòria: l'alternativa infravalorada
- Mockito 5: dependència i posada en marxa
- Crear mocks: anotacions i mètodes de fàbrica
@InjectMocksi les seves reserves- Definir comportament:
when(...).thenReturn(...) thenThrow: provar els camins d'errorthenAnswer: respostes dinàmiquesdoReturn,doThrow,doNothingi quan són obligatoris- Valors per defecte d'un mock
- Verificar interaccions:
verify times,never,atLeast,onlyinOrderiverifyNoMoreInteractions- El criteri: verificar ordres, no consultes
- Matchers:
any,eq,argThat - La regla de tots o cap
ArgumentCaptor- Espies amb
@Spy mockStatici per què és un senyal d'alarma- Què NO s'ha de simular
- El risc del mock excessiu
- Proves basades en estat enfront de basades en interacció
- El mateix cas, resolt de les dues formes
- Quan el disseny elimina la necessitat del mock
- Combinar Mockito amb AssertJ
- BiblioTech:
GestorPrestecsamb repositoris simulats - BiblioTech:
ServeiAvisosverificat ambArgumentCaptor - BiblioTech:
ClientMetadadesprovat sense xarxa - Proves d'integració:
@SpringBootTesti@MockitoBean @DataJpaTestamb H2- Testcontainers, presentat
- Cobertura: interpretació honesta
- Errors comuns i consells
- Exercicis
- Per què no n'hi ha prou amb JUnit
JUnit executa proves i comprova resultats. El que no fa és resoldre el problema dels col·laboradors.
GestorPrestecs depèn de cinc coses:
public GestorPrestecs(MaterialRepository materials,
EmpleatRepository empleats,
PrestecRepository prestecs,
ServeiAvisos avisos,
PropietatsBiblioTech propietats,
Clock rellotge) { ... }Per provar-lo necessites els sis. I tres d'ells són problemàtics:
| Col·laborador | Problema |
|---|---|
MaterialRepository |
Base de dades: lenta d'arrencar, cal preparar dades i netejar-les |
PrestecRepository |
Interfície de Spring Data amb vint mètodes heretats |
ServeiAvisos |
En producció envia correus de veritat |
ClientMetadades (09-06) |
Crida una API externa: lenta, no determinista, pot estar caiguda |
Les quatre raons per les quals un col·laborador destorba en una prova:
- Lent: base de dades, xarxa, disc.
- No determinista: rellotge, atzar, respostes d'una API que canvien.
- Difícil de posar en un estat concret: com fas que la base de dades falli exactament al tercer
INSERT? - Amb efectes reals: enviar correus, cobrar, esborrar.
Un doble de prova (test double) és un substitut que ocupa el lloc del col·laborador real i fa el que la prova necessita. El terme ve del cinema: el doble d'acció.
- Els cinc tipus de doble de prova
La taxonomia clàssica, amb definicions precises perquè a la pràctica es confonen constantment:
| Tipus | Definició | Per a què | Exemple a BiblioTech |
|---|---|---|---|
| Dummy | Es passa per omplir un paràmetre; mai no es fa servir | Satisfer una signatura | Un ServeiAvisos en una prova que no avisa ningú |
| Stub | Retorna respostes predefinides; no verifica res | Controlar el que entra al sistema sota prova | Repositori que sempre retorna "Java Eficaç" |
| Spy | És real o gairebé, i a més registra com se l'ha cridat | Verificar interaccions sense substituir tot | L'AvisosRegistrats que vas escriure a 11-04 |
| Mock | Doble amb expectatives sobre com s'ha de fer servir | Verificar interaccions | Verificar que es va avisar una sola vegada |
| Fake | Implementació real però simplificada | Substituir infraestructura completa | Repositori sobre un HashMap |
Vist d'una altra forma, segons el que aporta cadascun:
graph TD
A["Que necessito del col.laborador?"] --> B{"Es fa servir tan sols?"}
B -->|No| C["DUMMY<br/>qualsevol objecte val"]
B -->|Si| D{"Necessito que retorni<br/>dades concretes?"}
D -->|Si, i no comprovo com s'ha usat| E["STUB<br/>when().thenReturn()"]
D -->|Si, i a mes comprovo com s'ha usat| F["MOCK<br/>when() + verify()"]
D -->|No, nomes comprovo que es va cridar| G["MOCK o SPY<br/>verify()"]
A --> H{"Necessito comportament<br/>real i complet?"}
H -->|Si| I["FAKE<br/>implementacio en memoria"]
Un aclariment important sobre el vocabulari: a Mockito, tot s'anomena mock. Mockito.mock(X.class) crea un objecte que pots fer servir com a dummy, com a stub o com a mock segons el que hi facis. La distinció és conceptual, no d'API, però saber-la t'ajuda a raonar sobre què estàs fent — i sobre si hauries d'estar verificant o no.
- El fake en memòria: l'alternativa infravalorada
Abans d'entrar en Mockito, convé defensar l'opció que molta gent oblida.
Un fake és una implementació real però simplificada. La vas escriure a 11-04 sense donar-li nom:
class RepositoriEnMemoria implements RepositoriPrestecs {
private final Map<Long, Prestec> dades = new HashMap<>();
private long seguentId = 1;
@Override public Prestec save(Prestec p) {
if (p.getId() == null) p.assignarId(seguentId++);
dades.put(p.getId(), p);
return p;
}
@Override public Optional<Prestec> findById(Long id) {
return Optional.ofNullable(dades.get(id));
}
@Override public List<Prestec> findByEstat(EstatPrestec estat) {
return dades.values().stream().filter(p -> p.getEstat() == estat).toList();
}
}Comparat amb un mock:
| Aspecte | Mock (Mockito) | Fake en memòria |
|---|---|---|
| Cost de creació | Una línia | 30-80 línies, una vegada |
| Comportament | Només el que programis | Real i coherent |
| Estat entre crides | No n'hi ha, llevat que el simulis | Sí: deses i després llegeixes |
| Acoblament a la implementació | Alt: sap quins mètodes es criden | Baix |
| Fragilitat en refactoritzar | Alta | Baixa |
| Verificar interaccions | Sí | No, llevat que ho afegeixis |
| Reutilitzable entre proves | Es reconfigura cada vegada | S'escriu una vegada |
El cas on el fake guanya clarament:
// Amb MOCK: cal programar cada resposta, i no hi ha coherencia
when(repositori.save(any())).thenReturn(prestec);
when(repositori.findById(1L)).thenReturn(Optional.of(prestec));
when(repositori.countByEmpleat(marta)).thenReturn(1L);
// Si el codi desa i despres compta, el mock NO reflecteix el que es va desar.
// Cal mantenir a ma una coherencia que el fake dona gratis.
// Amb FAKE: deses i comptes, i surt el correcte
repositori.save(prestec);
assertThat(repositori.countByEmpleat(marta)).isEqualTo(1);Recomanació professional: per al repositori principal d'una aplicació, un fake en memòria ben fet sol ser millor inversió que vint mocks configurats en vint proves diferents. S'escriu una vegada, es reutilitza sempre i no es trenca en refactoritzar. Per a col·laboradors puntuals, o quan necessites provocar fallades, Mockito.
A la pràctica es fan servir tots dos, i saber triar és part de l'ofici.
- Mockito 5: dependència i posada en marxa
Ja el tens. spring-boot-starter-test (11-04, 11-05) porta mockito-core i mockito-junit-jupiter.
Sense Spring:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.12.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.12.0</version>
<scope>test</scope>
</dependency>L'estructura bàsica d'una prova amb Mockito:
package com.nexussoftware.bibliotech.servei;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.*;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class) // activa @Mock, @Spy, @Captor, @InjectMocks
class GestorPrestecsTest {
@Mock private MaterialRepository materials;
@Mock private EmpleatRepository empleats;
@Mock private PrestecRepository prestecs;
@Mock private ServeiAvisos avisos;
private GestorPrestecs gestor;
@BeforeEach
void preparar() {
// Construccio explicita: millor que @InjectMocks (apartat 6)
gestor = new GestorPrestecs(materials, empleats, prestecs, avisos,
propietatsDeProva(), RELLOTGE_FIX);
}
}@ExtendWith(MockitoExtension.class) és el mecanisme d'extensions de JUnit 5 (11-04). Fa tres coses: crea els mocks abans de cada prova, els reinicia entre proves, i valida l'ús en acabar —avisant de comportaments programats que mai no es van fer servir, cosa que sol indicar una prova mal escrita o codi mort—.
Com funciona per dins, i aquí torna el mòdul 10: Mockito fa servir ByteBuddy per generar en temps d'execució una subclasse de la classe o una implementació de la interfície, amb tots els mètodes sobreescrits per registrar la crida i retornar el que s'ha programat. És exactament el mecanisme de generació de bytecode de l'apartat 6 d'11-01, i és la raó de dues limitacions que veuràs: una classe final o un mètode final no es poden simular pel mecanisme clàssic (Mockito 5 ho permet amb un motor alternatiu, però per defecte no).
I aquest és el mateix ByteBuddy que va aparèixer a dependency:tree a 11-05, portat per Hibernate per als seus proxies de càrrega mandrosa. Tres eines diferents, el mateix mecanisme.
- Crear mocks: anotacions i mètodes de fàbrica
Dues formes equivalents:
// Forma 1: anotacio (recomanada, mes llegible)
@ExtendWith(MockitoExtension.class)
class ProvaAmbAnotacions {
@Mock private MaterialRepository materials;
}
// Forma 2: metode de fabrica (util per a mocks locals d'una prova)
class ProvaAmbFabrica {
@Test
void exemple() {
MaterialRepository materials = mock(MaterialRepository.class);
// ...
}
}Opcions útils en crear un mock:
// Amb nom: els missatges d'error l'identifiquen
MaterialRepository materials = mock(MaterialRepository.class, "repositoriDeMaterials");
// Respostes per defecte diferents
ServeiAvisos avisos = mock(ServeiAvisos.class, Answers.RETURNS_DEEP_STUBS);
// Llanca excepcio si es crida un metode no programat: forca a ser explicit
MaterialRepository estricte = mock(MaterialRepository.class, Answers.RETURNS_SMART_NULLS);Sobre RETURNS_DEEP_STUBS: permet when(a.getB().getC()).thenReturn(x) sense programar els intermedis. És gairebé sempre un senyal d'alarma: significa que el teu codi encadena crides a través de diversos objectes, cosa que viola la llei de Demeter. Evita'l i arregla el disseny.
@InjectMocks i les seves reserves
@InjectMocks i les seves reservesMockito pot construir l'objecte sota prova i injectar-li els mocks:
@ExtendWith(MockitoExtension.class)
class GestorPrestecsTest {
@Mock private MaterialRepository materials;
@Mock private ServeiAvisos avisos;
@InjectMocks private GestorPrestecs gestor; // construit automaticament
}Còmoda, però té inconvenients reals que convé conèixer:
- Falla en silenci. Si Mockito no troba un mock per a un paràmetre, injecta
nullsense avisar. La prova falla després amb unNullPointerExceptionla causa del qual no és evident. - No admet valors no simulables. El
Clock.fixedi elPropietatsBiblioTechde BiblioTech no són mocks: són objectes reals.@InjectMocksno els posa. - Amaga el constructor. Un dels beneficis de la injecció per constructor (11-02) és que un constructor amb nou paràmetres crida "aquesta classe fa massa".
@InjectMocksamaga exactament aquest senyal.
Recomanació: construeix l'objecte explícitament a @BeforeEach.
@BeforeEach
void preparar() {
gestor = new GestorPrestecs(materials, empleats, prestecs, avisos,
propietatsDeProva(), RELLOTGE_FIX);
}És una línia més, és explícit, permet barrejar mocks amb objectes reals i falla en compilació si canvia el constructor. Molts equips amb experiència han abandonat @InjectMocks per aquests motius.
- Definir comportament:
when(...).thenReturn(...)
when(...).thenReturn(...)L'operació central. Es coneix com a stubbing.
@Test
void prestarRetornaUnPrestecAmbVencimentAQuinzeDies() {
// PREPARAR: programar el que retornen els col.laboradors
Material llibre = new Llibre("978-0000000001", "Java Eficac", 3, "J. Bloch");
Empleat marta = new Empleat("[email protected]", "Marta Ruiz");
when(materials.findByIsbn("978-0000000001")).thenReturn(Optional.of(llibre));
when(empleats.findByCorreu("[email protected]")).thenReturn(Optional.of(marta));
when(prestecs.countByEmpleatAndEstat(marta, EstatPrestec.ACTIU)).thenReturn(0L);
when(prestecs.save(any(Prestec.class))).thenAnswer(inv -> inv.getArgument(0));
// ACTUAR
Prestec prestec = gestor.prestar("978-0000000001", "[email protected]");
// COMPROVAR
assertThat(prestec.getDataVenciment())
.isEqualTo(LocalDate.of(2026, 3, 20).plusDays(15));
}Variants de thenReturn:
// Valors diferents en crides successives
when(prestecs.count())
.thenReturn(0L) // 1a crida
.thenReturn(1L) // 2a
.thenReturn(2L); // 3a i seguents
// Equivalent, mes curt
when(prestecs.count()).thenReturn(0L, 1L, 2L);Un detall sobre when: no és màgia, és un truc. when(materials.findByIsbn("...")) crida de veritat el mètode del mock, que registra internament la invocació i retorna null. when recupera aquesta última invocació registrada i li associa el comportament. Saber això explica per què when no funciona amb mètodes void (no hi ha res per passar a when) i per què cal fer servir la forma do... amb espies (apartat 10).
thenThrow: provar els camins d'error
thenThrow: provar els camins d'errorAquí hi ha una de les raons més fortes per fer servir Mockito. Provar què passa quan la base de dades falla és gairebé impossible amb una implementació real; amb un mock és una línia.
@Test
void siElRepositoriFallaEnDesarNoSEnviaCapAvis() {
when(materials.findByIsbn(anyString())).thenReturn(Optional.of(unLlibre()));
when(empleats.findByCorreu(anyString())).thenReturn(Optional.of(marta()));
when(prestecs.countByEmpleatAndEstat(any(), any())).thenReturn(0L);
// El repositori falla en desar
when(prestecs.save(any()))
.thenThrow(new DataAccessResourceFailureException("Connexio perduda"));
assertThatThrownBy(() -> gestor.prestar("978-0000000001", "[email protected]"))
.isInstanceOf(DataAccessResourceFailureException.class);
// L'important: NO es va avisar ningu d'un prestec que no es va desar
verifyNoInteractions(avisos);
}Aquesta prova verifica una propietat de negoci real: no es notifica el que no s'ha persistit. Sense Mockito, provocar-la exigiria desconnectar la base de dades a mitja prova.
Altres escenaris de fallada que ara pots provar:
// Excepcio de concurrencia (el bloqueig optimista d'11-03)
when(materials.findByIsbn(anyString()))
.thenThrow(new OptimisticLockingFailureException("Versio obsoleta"));
// Temps d'espera esgotat a la xarxa (el ClientMetadades de 09-06)
when(clientHttp.send(any(), any()))
.thenThrow(new HttpTimeoutException("L'API no respon"));
// Fallada la primera vegada, exit la segona: provar el reintent
when(prestecs.save(any()))
.thenThrow(new OptimisticLockingFailureException("conflicte"))
.thenReturn(prestecDesat);Aquest últim patró és exactament el que necessites per provar el bucle de reintents que vas escriure a 11-03. Sense Mockito, no hi ha forma raonable de fer-ho.
thenAnswer: respostes dinàmiques
thenAnswer: respostes dinàmiquesQuan la resposta depèn dels arguments:
// El cas mes comu: save retorna el que se li va passar
when(prestecs.save(any(Prestec.class))).thenAnswer(inv -> inv.getArgument(0));
// Simular l'assignacio d'id que faria la base de dades
when(prestecs.save(any(Prestec.class))).thenAnswer(invocacio -> {
Prestec p = invocacio.getArgument(0);
if (p.getId() == null) {
p.assignarId(seguentId.getAndIncrement());
}
return p;
});
// Resposta segons l'argument
when(materials.findByIsbn(anyString())).thenAnswer(invocacio -> {
String isbn = invocacio.getArgument(0);
return catalegDeProva.containsKey(isbn)
? Optional.of(catalegDeProva.get(isbn))
: Optional.empty();
});Avís: quan el teu Answer comença a tenir lògica, estàs escrivint un fake dins d'un mock. En aquest punt, escriu un fake de veritat (apartat 3): serà més llegible i reutilitzable.
doReturn, doThrow, doNothing i quan són obligatoris
doReturn, doThrow, doNothing i quan són obligatorisExisteix una segona sintaxi que inverteix l'ordre:
// Sintaxi when: l'habitual
when(materials.findByIsbn("x")).thenReturn(Optional.empty());
// Sintaxi do: equivalent, pero el metode s'anomena al final
doReturn(Optional.empty()).when(materials).findByIsbn("x");No són intercanviables sempre. Hi ha tres casos on do... és obligatori:
Cas 1: mètodes void
// NO COMPILA: avisar() retorna void, no hi ha res per passar a when()
when(avisos.avisar(marta, "missatge")).thenThrow(new RuntimeException());
// CORRECTE
doThrow(new ServeiAvisosException("SMTP caigut"))
.when(avisos).avisar(any(Empleat.class), anyString());
// I perque un void no faci res (es el comportament per defecte,
// pero explicitar-ho de vegades aclareix la intencio)
doNothing().when(avisos).avisar(any(), anyString());Cas 2: espies
List<String> llista = new ArrayList<>();
List<String> espia = spy(llista);
// MALAMENT: when() CRIDA DE VERITAT get(0) sobre una llista buida
// -> IndexOutOfBoundsException abans de programar res
when(espia.get(0)).thenReturn("Java Eficac");
// BE: do... NO invoca el metode real
doReturn("Java Eficac").when(espia).get(0);Aquest és el parany número u dels espies, i s'entén amb el de l'apartat 7: when(x.metode()) executa x.metode(). En un mock pur això és inofensiu perquè no hi ha implementació real; en un espia, executa el codi real amb totes les seves conseqüències.
Cas 3: reprogramar un comportament ja definit
when(materials.findByIsbn(anyString())).thenReturn(Optional.of(llibre));
// Mes endavant, canviar el comportament:
doReturn(Optional.empty()).when(materials).findByIsbn("978-9999999999");Resum:
| Situació | Sintaxi |
|---|---|
| Mètode normal en un mock | when(...).thenReturn(...) — més llegible |
Mètode void |
doThrow / doNothing |
Espia (@Spy) |
doReturn(...).when(espia).metode() |
| Reprogramar | doReturn |
Regla mnemotècnica: fes servir when per defecte; canvia a do... quan when no compili o quan treballis amb un espia.
- Valors per defecte d'un mock
Un mock acabat de crear respon a tot, sense necessitat de programar-ho:
| Tipus de retorn | Retorna per defecte |
|---|---|
int, long, double |
0 |
boolean |
false |
Object i qualsevol classe |
null |
String |
null |
List, Set, Map |
Col·lecció buida (no null) |
Optional |
Optional.empty() (des de Mockito 2) |
Stream |
Stream buit |
void |
No fa res |
Aquests valors per defecte són molt convenients. Que Optional retorni Optional.empty() i que les col·leccions retornin buit evita la majoria dels NullPointerException en proves on no t'importa aquell col·laborador.
Pràctica útil: només programa el que la prova necessita de veritat. Si GestorPrestecs crida cinc mètodes del repositori però la teva prova només depèn de dos, programa'n dos. Menys configuració, prova més llegible i menys fràgil.
I aquí entra la validació estricta de MockitoExtension:
org.mockito.exceptions.misusing.UnnecessaryStubbingException:
Unnecessary stubbings detected.
Clean & maintainable test code requires zero unnecessary code.
1. -> at GestorPrestecsTest.preparar(GestorPrestecsTest.java:44)Aquesta excepció no és una molèstia: t'està dient que vas programar un comportament que mai no es va fer servir. O la prova no fa el que creus, o hi ha codi mort. Val la pena investigar-ho en comptes de silenciar-ho amb @MockitoSettings(strictness = Strictness.LENIENT).
- Verificar interaccions:
verify
verifyFins aquí, els dobles servien per controlar el que entra. verify serveix per comprovar el que surt: quines crides va fer l'objecte sota prova als seus col·laboradors.
@Test
void retornarAmbRetardRegistraLaMultaIAvisaLEmpleat() {
Prestec vencut = prestecVencutFa(10);
when(prestecs.findById(1L)).thenReturn(Optional.of(vencut));
gestor.retornar(1L);
// Es va desar el prestec actualitzat
verify(prestecs).save(vencut);
// Es va avisar l'empleat
verify(avisos).avisar(eq(vencut.getEmpleat()), contains("multa"));
}verify(mock).metode(args) significa: "comprova que es va cridar metode amb aquests arguments exactament una vegada".
És imprescindible quan l'efecte que vols comprovar no és al valor de retorn. Enviar un avís, publicar un esdeveniment, esborrar un fitxer: són efectes, i només es poden verificar així.
times, never, atLeast, only
times, never, atLeast, onlyverify(avisos, times(1)).avisar(any(), anyString()); // exactament 1 (per defecte)
verify(avisos, times(3)).avisar(any(), anyString()); // exactament 3
verify(avisos, never()).avisar(any(), anyString()); // MAI
verify(avisos, atLeastOnce()).avisar(any(), anyString()); // 1 o mes
verify(avisos, atLeast(2)).avisar(any(), anyString()); // 2 o mes
verify(avisos, atMost(5)).avisar(any(), anyString()); // 5 o menys
verify(avisos, only()).avisar(any(), anyString()); // aquesta crida I CAP ALTRA
verifyNoInteractions(avisos); // el mock no es va fer servir en absolut
verifyNoMoreInteractions(prestecs); // no hi va haver mes crides de les verificadesnever() és probablement el més valuós de tots, perquè comprova que alguna cosa no va passar:
@Test
void unPrestecRetornatEnTerminiNoGeneraAvis() {
when(prestecs.findById(1L)).thenReturn(Optional.of(prestecEnTermini()));
gestor.retornar(1L);
verify(avisos, never()).avisar(any(), anyString());
}Comprovar absències és impossible amb assercions sobre el resultat, i és on verify aporta valor únic.
inOrder i verifyNoMoreInteractions
inOrder i verifyNoMoreInteractionsQuan l'ordre importa:
@Test
void esDecrementaLEstocAbansDeDesarElPrestec() {
InOrder ordre = inOrder(materials, prestecs, avisos);
gestor.prestar("978-0000000001", "[email protected]");
ordre.verify(materials).findByIsbn("978-0000000001");
ordre.verify(prestecs).save(any(Prestec.class));
ordre.verify(avisos).avisar(any(), anyString());
}Avís: verificar l'ordre és una de les formes més ràpides de crear una prova fràgil. Fes-ho només quan l'ordre sigui un requisit de negoci real —per exemple, "l'avís s'ha d'enviar després de confirmar el desat, mai abans"—, no perquè el codi actual ho faci així.
verifyNoMoreInteractions comprova que no hi va haver crides addicionals:
gestor.consultar("978-0000000001");
verify(materials).findByIsbn("978-0000000001");
verifyNoMoreInteractions(materials); // no es va cridar res mesFes-lo servir amb moderació: qualsevol crida nova i legítima —afegir una mètrica, una traça— trenca la prova sense que el comportament hagi canviat.
- El criteri: verificar ordres, no consultes
Aquest és l'apartat més important de la lliçó, i el que separa les proves bones de les fràgils.
La distinció ve de la separació entre ordres i consultes (command-query separation):
| Consulta (query) | Ordre (command) | |
|---|---|---|
| Què fa | Retorna dades, sense efectes | Produeix un efecte, normalment sense retornar res |
| Exemples | findByIsbn, countByEmpleat, calcular |
save, avisar, delete, publicar |
| Com provar-ho | Amb un stub: programa el que retorna | Amb verify: comprova que es va cridar |
| Verificar? | NO | SÍ |
Regla: programa les consultes, verifica les ordres.
Per què no es verifiquen les consultes:
// MALAMENT: verificar una consulta
verify(materials).findByIsbn("978-0000000001");
verify(prestecs).countByEmpleatAndEstat(marta, EstatPrestec.ACTIU);Aquestes dues línies diuen "el codi crida aquests mètodes". Però això és implementació, no comportament. Si demà optimitzes GestorPrestecs per fer una sola consulta combinada en comptes de dues, el resultat serà idèntic i la prova es trencarà. Això és exactament l'antipatró de prova fràgil d'11-04.
A més és redundant: si el codi no hagués cridat findByIsbn, no tindria el material i el resultat seria diferent. L'asserció sobre el resultat ja ho comprova, indirectament i sense acoblar-se.
Per què sí que es verifiquen les ordres:
// BE: verificar una ordre
verify(avisos).avisar(marta, "El teu prestec de Java Eficac venc en 2 dies");Enviar un avís és un efecte observable que el sistema ha de produir. No apareix en cap valor de retorn. L'única forma de comprovar-ho és verificar la interacció, i és un requisit de negoci genuí: "quan faltin dos dies, s'avisa l'empleat".
Aplicat a BiblioTech:
| Col·laborador | Mètode | Tipus | Què fer |
|---|---|---|---|
MaterialRepository |
findByIsbn |
Consulta | Programar |
PrestecRepository |
countByEmpleatAndEstat |
Consulta | Programar |
PrestecRepository |
save |
Ordre | Verificar |
ServeiAvisos |
avisar |
Ordre | Verificar |
CalculadoraMultes |
calcular |
Consulta | Programar (o fer servir la real) |
RegistreAuditoria |
anotar |
Ordre | Verificar |
Si apliques només aquesta regla, les teves proves amb Mockito seran molt millors que la mitjana.
- Matchers:
any, eq, argThat
any, eq, argThatQuan no vols —o no pots— fixar l'argument exacte:
| Matcher | Coincideix amb |
|---|---|
any() |
Qualsevol cosa, inclòs null |
any(Prestec.class) |
Qualsevol Prestec no nul |
anyString(), anyInt(), anyLong() |
Qualsevol valor d'aquest tipus, no nul |
anyList(), anyMap(), anyCollection() |
Qualsevol col·lecció |
eq(valor) |
Aquest valor exacte (amb equals) |
isNull(), isNotNull() |
Nul·litat |
contains("text") |
String que conté aquest text |
startsWith, endsWith, matches("regex") |
Coincidències de cadena |
argThat(predicat) |
El que tu defineixis |
when(materials.findByIsbn(anyString())).thenReturn(Optional.of(llibre));
verify(avisos).avisar(any(Empleat.class), contains("venc"));
// argThat: condicions arbitraries
verify(prestecs).save(argThat(p ->
p.getEstat() == EstatPrestec.ACTIU
&& p.getDataVenciment().equals(LocalDate.of(2026, 4, 4))));Consell important sobre any(): és còmode i debilita la prova. verify(avisos).avisar(any(), any()) comprova que es va avisar algú d'alguna cosa. Si el bug és que s'avisa l'empleat equivocat, la prova no ho detecta. Sigues tot el específic que puguis; fes servir any() només per al que de veritat és irrellevant.
- La regla de tots o cap
Un error que comet tothom la primera vegada:
org.mockito.exceptions.misusing.InvalidUseOfMatchersException:
Invalid use of argument matchers!
2 matchers expected, 1 recorded.
This exception may occur if matchers are combined with raw values:
//incorrect:
someMethod(anyObject(), "raw String");
When using matchers, all arguments have to be provided by matchers.Regla: si fas servir un matcher en un argument, TOTS els arguments han de ser matchers.
La correcció és embolcallar els valors literals en eq():
// CORRECTE
verify(avisos).avisar(eq(marta), anyString());
// Tambe correcte: cap matcher
verify(avisos).avisar(marta, "El teu prestec venc en 2 dies");
// Tambe correcte: tots matchers
verify(avisos).avisar(any(Empleat.class), anyString());Per què passa: els matchers no són valors, són efectes secundaris sobre una pila interna. Quan escrius anyString(), Mockito apila un matcher i retorna null. En processar la invocació, compara el nombre de matchers apilats amb el nombre d'arguments del mètode. Si no coincideixen, no pot saber quin correspon a quin, i falla.
Conseqüència pràctica i desconcertant: l'error pot aparèixer a la línia següent, o fins i tot a la prova següent, perquè la pila queda contaminada. Si veus un InvalidUseOfMatchersException en un lloc on no fas servir matchers, mira la prova anterior.
ArgumentCaptor
ArgumentCaptorverify amb matchers respon a "es va cridar amb alguna cosa així?". ArgumentCaptor respon a "amb què exactament es va cridar?".
@ExtendWith(MockitoExtension.class)
class ServeiAvisosVencimentTest {
@Mock private PrestecRepository prestecs;
@Mock private ServeiAvisos avisos;
@Captor private ArgumentCaptor<String> capturadorMissatge;
@Captor private ArgumentCaptor<Empleat> capturadorEmpleat;
@Test
void elMissatgeDAvisInclouElTitolIElsDiesRestants() {
Prestec prestec = prestecQueVenc(AVUI.plusDays(2), marta(), "Java Eficac");
when(prestecs.cercarActius()).thenReturn(List.of(prestec));
servei.enviarAvisosDelDia();
// Capturar els arguments REALS de la crida
verify(avisos).avisar(capturadorEmpleat.capture(), capturadorMissatge.capture());
assertThat(capturadorEmpleat.getValue().getCorreu())
.isEqualTo("[email protected]");
assertThat(capturadorMissatge.getValue())
.contains("Java Eficac")
.contains("2 dies")
.doesNotContain("null"); // el classic bug de concatenacio
}
}Aquest doesNotContain("null") mereix un comentari: és una comprovació petita que detecta un dels bugs més freqüents i més visibles per a l'usuari final — un correu que diu "El teu préstec de null venç aviat".
Capturar diverses crides:
@Test
void sAvisaCadaEmpleatAmbPrestecProximAVencer() {
when(prestecs.cercarActius()).thenReturn(List.of(
prestecQueVenc(AVUI.plusDays(2), marta(), "Java Eficac"),
prestecQueVenc(AVUI.plusDays(2), diego(), "Patrons de Disseny"),
prestecQueVenc(AVUI.plusDays(9), nuria(), "Refactoritzacio"))); // encara no toca
servei.enviarAvisosDelDia();
verify(avisos, times(2)).avisar(capturadorEmpleat.capture(), capturadorMissatge.capture());
assertThat(capturadorEmpleat.getAllValues())
.extracting(Empleat::getCorreu)
.containsExactly("[email protected]",
"[email protected]");
assertThat(capturadorMissatge.getAllValues())
.allSatisfy(missatge -> assertThat(missatge).contains("2 dies"));
}Nota: getValue() retorna l'última captura; getAllValues() retorna totes en ordre.
ArgumentCaptor enfront d'argThat:
ArgumentCaptor |
argThat |
|
|---|---|---|
| Quan es comprova | Després de verify |
Durant verify |
| Missatge d'error | Clar: diu quin valor hi havia | Pobre: "no hi va haver crida coincident" |
| Assercions complexes | Sí, amb AssertJ complet | Limitat a un predicat |
| Verbositat | Més gran | Menor |
Per a comprovacions no trivials, prefereix ArgumentCaptor, precisament pels missatges d'error. Amb argThat, quan falla només saps que cap crida no va complir el predicat; amb el captor, veus exactament què va arribar.
- Espies amb
@Spy
@SpyUn espia embolcalla un objecte real: per defecte executa el codi real, i pots substituir mètodes concrets.
@ExtendWith(MockitoExtension.class)
class CalculadoraMultesTest {
@Spy private CalculadoraMultes calculadora =
new CalculadoraMultes(propietatsDeProva(), RELLOTGE_FIX);
@Test
void exemple() {
// Comportament REAL per defecte
assertThat(calculadora.calcular(prestecAmbRetard(5))).isEqualByComparingTo("2.50");
// Substituir NOMES un metode (nota: do... obligatori, apartat 10)
doReturn(new BigDecimal("999.99")).when(calculadora).calcular(any());
assertThat(calculadora.calcular(prestecAmbRetard(5))).isEqualByComparingTo("999.99");
}
}Casos d'ús legítims:
- Codi heretat que no es pot refactoritzar i cal aïllar un mètode.
- Verificar crides sobre un objecte real sense substituir el seu comportament.
- Substituir un únic mètode car d'una classe per la resta barata.
I l'advertiment, que és seriós:
Necessitar un espia sobre la teva pròpia classe gairebé sempre indica un problema de disseny.
Si has de substituir un mètode de la classe que estàs provant, aquesta classe fa dues coses: la que proves i la que substitueixes. La solució correcta és extreure la segona a un col·laborador i injectar-la. Llavors és un mock normal, i el disseny ha millorat.
Dues trampes tècniques dels espies, a més de la de when (apartat 10):
// TRAMPA: l'espia es una COPIA, no l'objecte original
List<String> original = new ArrayList<>();
List<String> espia = spy(original);
espia.add("Java Eficac");
assertThat(espia).hasSize(1); // ok
assertThat(original).isEmpty(); // l'original NO va canviar!// TRAMPA: les crides internes NO passen per l'espia
// Es exactament el problema del proxy d'11-02 §26.
// Si metodeA() crida this.metodeB(), substituir metodeB no afecta metodeA.Aquesta segona és el mateix parany de la crida interna de @Transactional. El mecanisme és idèntic: un proxy només intercepta el que passa per ell.
mockStatic i per què és un senyal d'alarma
mockStatic i per què és un senyal d'alarmaDes de Mockito 3.4 es poden simular mètodes estàtics:
@Test
void exempleAmbEstatic() {
try (MockedStatic<LocalDate> data = mockStatic(LocalDate.class)) {
data.when(LocalDate::now).thenReturn(LocalDate.of(2026, 3, 20));
// dins d'aquest bloc, LocalDate.now() retorna la data fixa
assertThat(LocalDate.now()).isEqualTo(LocalDate.of(2026, 3, 20));
}
// fora, comportament normal
}El try-with-resources (06-06) és obligatori: la simulació és per fil i cal desfer-la, o contaminarà les proves següents.
I ara la part important:
Necessitar
mockStaticés gairebé sempre el senyal que hi ha un problema de disseny.
Si has de simular LocalDate.now(), és que el teu codi el crida directament en comptes de fer servir un Clock injectat. La solució no és el mock estàtic: és injectar el Clock, exactament el que vas fer a 10-05 i vas cobrar a 11-04.
Comparació honesta del mateix problema:
// Opcio A: mockStatic. Funciona, i arrossega problemes.
try (MockedStatic<LocalDate> f = mockStatic(LocalDate.class)) {
f.when(LocalDate::now).thenReturn(LocalDate.of(2026, 3, 20));
assertThat(calculadora.calcular(prestec)).isEqualByComparingTo("2.50");
}
// Opcio B: Clock injectat. El disseny elimina la necessitat del mock.
var calculadora = new CalculadoraMultes(propietats, Clock.fixed(...));
assertThat(calculadora.calcular(prestec)).isEqualByComparingTo("2.50");mockStatic |
Clock injectat |
|
|---|---|---|
| Canvi en producció | Cap | Un paràmetre més |
| Llegibilitat de la prova | Pitjor | Millor |
| Rendiment | Lent (instrumenta la classe) | Nul |
| Risc de contaminar altres proves | Sí, si oblides tancar | No |
| Funciona en paral·lel | Amb cura | Sí |
| Millora el disseny | No | Sí |
Quan mockStatic és acceptable: codi heretat que no pots canviar, o una utilitat estàtica de tercers que no té alternativa injectable. En codi nou, arregla el disseny.
- Què NO s'ha de simular
Una llista curta que evita molt de dolor:
| No simulis | Per què | Què fer |
|---|---|---|
| Tipus que no controles | Simules la teva creença sobre com es comporten, no com es comporten. Si t'equivoques, la prova passa i el codi falla | Embolcallar-los en una interfície pròpia i simular aquesta |
Classes de valor (String, LocalDate, BigDecimal, els teus record) |
Són barates de crear i el seu comportament és el que vols | Fer servir instàncies reals |
| El framework | Simular l'EntityManager prova la teva idea de JPA, no JPA |
Prova d'integració amb H2 |
| Col·leccions del JDK | Un List simulat és més fràgil i més lent que un ArrayList |
Fer servir el real |
| La classe sota prova | Si has de simular part d'ella, fa massa | Extreure un col·laborador |
| Objectes de dades sense lògica | No hi ha res per simular | Construir-los |
El primer mereix desenvolupament, perquè és el més subtil. Imagina que simules HttpClient de Java 11 directament:
Aquesta prova passa. Però es recolza en la teva suposició que send llança IOException en determinat cas, que la resposta té aquell format exacte, que els codis d'estat arriben de certa manera. Si t'equivoques en qualsevol d'aquestes suposicions, la prova estarà verda mentre el codi falla en producció. És el pitjor resultat possible: confiança injustificada.
L'alternativa correcta: embolcallar la dependència externa en una interfície pròpia que expressa el que el teu domini necessita, i simular aquesta. És el que es fa a l'apartat 29 amb ClientMetadades.
- El risc del mock excessiu
Mira aquesta prova, que sembla minuciosa:
@Test
void prestar() {
when(materials.findByIsbn("978-0000000001")).thenReturn(Optional.of(llibre));
when(empleats.findByCorreu("[email protected]")).thenReturn(Optional.of(marta));
when(prestecs.countByEmpleatAndEstat(marta, ACTIU)).thenReturn(0L);
when(propietats.prestec()).thenReturn(new Prestec(15, 3));
when(calculadora.calcular(any())).thenReturn(BigDecimal.ZERO);
when(prestecs.save(any())).thenReturn(prestecDesat);
gestor.prestar("978-0000000001", "[email protected]");
verify(materials).findByIsbn("978-0000000001");
verify(empleats).findByCorreu("[email protected]");
verify(prestecs).countByEmpleatAndEstat(marta, ACTIU);
verify(prestecs).save(any());
verify(avisos).avisar(any(), anyString());
verifyNoMoreInteractions(materials, empleats, prestecs, avisos);
}És un exemple de manual de prova fràgil. Els seus problemes:
- Descriu la implementació, no el comportament. És una transcripció del cos del mètode.
- Es trenca en refactoritzar. Combinar dues consultes en una, guardar l'empleat en memòria cau, canviar l'ordre: tot la trenca, sense que el comportament canviï.
- No comprova el resultat. Ni una asserció sobre el préstec retornat. Podria retornar
nulli la prova passaria. - Simula coses que no hauria.
propietatsés unrecordimmutable icalculadoraés una funció pura: tots dos són barats de fer servir de veritat. - És il·legible. Dotze línies de configuració per provar un cas.
- Dóna falsa confiança. Està verda, i no garanteix gairebé res.
I el problema més profund: si el mètode està malament, la prova continua en verd. Pot calcular malament el venciment, posar l'estat equivocat o assignar el material incorrecte. Res d'això no es comprova.
La mateixa prova, ben feta:
@Test
void prestarCreaUnPrestecActiuAmbVencimentAQuinzeDies() {
when(materials.findByIsbn("978-0000000001")).thenReturn(Optional.of(llibre));
when(empleats.findByCorreu("[email protected]")).thenReturn(Optional.of(marta));
when(prestecs.save(any(Prestec.class))).thenAnswer(inv -> inv.getArgument(0));
Prestec prestec = gestor.prestar("978-0000000001", "[email protected]");
// COMPORTAMENT: que es va produir
assertThat(prestec)
.satisfies(p -> {
assertThat(p.getMaterial()).isEqualTo(llibre);
assertThat(p.getEmpleat()).isEqualTo(marta);
assertThat(p.getEstat()).isEqualTo(EstatPrestec.ACTIU);
assertThat(p.getDataVenciment()).isEqualTo(LocalDate.of(2026, 4, 4));
});
// EFECTES observables (ordres)
verify(prestecs).save(prestec);
verify(avisos).avisar(eq(marta), contains("Java Eficac"));
}Més curta, més llegible, comprova el que importa i sobreviu a qualsevol refactorització que no canviï el comportament.
- Proves basades en estat enfront de basades en interacció
Dues filosofies, i convé tenir-les clares:
| Basada en estat | Basada en interacció | |
|---|---|---|
| Què comprova | El resultat i l'estat final | Les crides als col·laboradors |
| Eina | Assercions (AssertJ) | verify de Mockito |
| Acoblament a la implementació | Baix | Alt |
| Resistència a refactoritzar | Alta | Baixa |
| Missatges d'error | Clars: "esperava X, hi havia Y" | Pitjors: "no hi va haver aquesta crida" |
| Quan és l'única opció | — | Efectes sense retorn |
Recomanació: prefereix les proves basades en estat. Fes servir les basades en interacció només quan l'efecte no sigui observable d'una altra forma.
Casos on la interacció és l'única opció:
- Enviar un correu o una notificació.
- Publicar un esdeveniment o un missatge en una cua.
- Registrar en un sistema d'auditoria extern.
- Comprovar que alguna cosa no va passar (
never()). - Comprovar que es va cridar exactament una vegada (idempotència, evitar duplicats).
- El mateix cas, resolt de les dues formes
Requisit de BiblioTech: "En retornar un préstec amb retard, es registra la multa corresponent."
Versió basada en interacció
@Test
void retornarAmbRetardRegistraLaMulta_interaccio() {
Prestec vencut = prestecVencutFa(10);
when(prestecs.findById(1L)).thenReturn(Optional.of(vencut));
when(calculadora.calcular(vencut)).thenReturn(new BigDecimal("5.00"));
gestor.retornar(1L);
verify(calculadora).calcular(vencut);
verify(multes).registrar(eq(vencut), eq(new BigDecimal("5.00")));
}Problemes: depèn que es faci servir CalculadoraMultes i que el registre sigui amb aquests dos arguments. Si demà el càlcul es mou a l'entitat Prestec —una refactorització molt raonable—, el comportament és idèntic i la prova es trenca. I no comprova si la multa registrada és correcta: comprova que es va passar el valor que el mateix mock va retornar, cosa que és circular.
Versió basada en estat
@Test
void retornarAmbRetardRegistraLaMulta_estat() {
// Col.laboradors REALS per al que es barat i determinista
var calculadora = new CalculadoraMultes(propietatsDeProva(), RELLOTGE_FIX);
var multesEnMemoria = new RegistreMultesEnMemoria(); // fake
var gestor = new GestorPrestecs(prestecs, calculadora, multesEnMemoria, avisos, RELLOTGE_FIX);
Prestec vencut = prestecVencutFa(10);
when(prestecs.findById(1L)).thenReturn(Optional.of(vencut));
gestor.retornar(1L);
// Es comprova l'ESTAT resultant
assertThat(multesEnMemoria.perEmpleat(vencut.getEmpleat()))
.singleElement()
.satisfies(multa -> {
assertThat(multa.quantitat()).isEqualByComparingTo("5.00");
assertThat(multa.prestec()).isEqualTo(vencut);
});
assertThat(vencut.getEstat()).isEqualTo(EstatPrestec.RETORNAT);
}Avantatges: comprova que la multa val realment 5,00 €, calculada pel codi real. Sobreviu que el càlcul es mogui de lloc. I comprova també l'estat del préstec, que l'altra ignorava.
La recomanació
| Situació | Enfocament |
|---|---|
| Hi ha un valor de retorn | Estat: assercions sobre ell |
| Hi ha un estat final observable | Estat: amb un fake si cal |
El col·laborador és barat i determinista (calculadora, record, Clock) |
Fer-lo servir de veritat, no simular-lo |
| L'efecte és extern (correu, esdeveniment, cua) | Interacció: verify |
| Cal comprovar que alguna cosa no va passar | Interacció: never() |
| El col·laborador és lent, no determinista o falla | Mock per controlar-lo |
A la pràctica, una bona prova barreja: simula el que destorba, fa servir el real per al que és barat, comprova l'estat i verifica només els efectes externs.
- Quan el disseny elimina la necessitat del mock
La lliçó més valuosa de tot el mòdul de proves, i ja la coneixes.
El cas del rellotge:
// Sense Clock: cal simular un metode estatic
try (MockedStatic<LocalDate> f = mockStatic(LocalDate.class)) {
f.when(LocalDate::now).thenReturn(LocalDate.of(2026, 3, 20));
// ... prova lenta, fragil i amb risc de contaminacio
}
// Amb Clock injectat: NO cal cap mock
var calculadora = new CalculadoraMultes(propietats, Clock.fixed(...));El disseny va eliminar la necessitat del doble. No és un truc de proves: és que un Clock injectat és millor disseny, i la facilitat de prova és una conseqüència.
Aquest patró es repeteix:
| Problema | Solució amb mock | Solució de disseny |
|---|---|---|
| El temps | mockStatic(LocalDate.class) |
Clock injectat |
| L'atzar | mockStatic(Math.class) |
Random injectat amb llavor |
| Els identificadors | mockStatic(UUID.class) |
Un GeneradorIds injectat |
| La configuració | Simular ReglesNegoci (impossible: és estàtic) |
@ConfigurationProperties injectat (11-02) |
| Un càlcul dins d'un servei | Simular el mateix servei amb @Spy |
Extreure'l a una funció pura |
| La base de dades | Simular el repositori | Un fake en memòria reutilitzable |
L'heurística general:
Abans d'escriure un mock, pregunta't si el problema és que el disseny necessita canviar. Si un col·laborador és difícil de simular, moltes vegades és perquè no hauria de ser un col·laborador.
Mockito és una eina excel·lent. I com tota eina potent, es pot fer servir per treballar al voltant d'un problema en lloc de resoldre'l.
- Combinar Mockito amb AssertJ
Es complementen perfectament: Mockito controla i captura, AssertJ comprova.
@Test
void lAvisConteTotesLesDadesDelPrestec() {
when(prestecs.cercarActius()).thenReturn(List.of(prestecDeMarta(), prestecDeDiego()));
servei.enviarAvisosDelDia();
ArgumentCaptor<Avis> captor = ArgumentCaptor.forClass(Avis.class);
verify(avisos, times(2)).enviar(captor.capture());
// AssertJ sobre el capturat: expressiu i amb bons missatges d'error
assertThat(captor.getAllValues())
.hasSize(2)
.extracting(Avis::destinatari, Avis::assumpte)
.containsExactly(
tuple("[email protected]", "El teu prestec venc en 2 dies"),
tuple("[email protected]","El teu prestec venc en 2 dies"));
assertThat(captor.getAllValues())
.allSatisfy(avis -> {
assertThat(avis.cos()).doesNotContain("null");
assertThat(avis.destinatari()).endsWith("@nexussoftware.com");
});
}AssertJ ofereix a més assertThatThrownBy combinat amb thenThrow:
when(prestecs.save(any())).thenThrow(new DataAccessResourceFailureException("Sense connexio"));
assertThatThrownBy(() -> gestor.prestar("978-0000000001", "[email protected]"))
.isInstanceOf(BiblioTechException.class) // es tradueix l'excepcio
.hasCauseInstanceOf(DataAccessResourceFailureException.class)
.hasMessageContaining("978-0000000001");Aquesta prova verifica l'estratègia de gestió d'errors per capes del mòdul 6: l'excepció tècnica s'embolcalla en una de domini, conservant la causa.
- BiblioTech:
GestorPrestecs amb repositoris simulats
GestorPrestecs amb repositoris simulatsLa prova completa, aplicant tot el criteri de la lliçó:
package com.nexussoftware.bibliotech.servei;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.math.BigDecimal;
import java.time.*;
import java.util.*;
import org.junit.jupiter.api.*;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
@DisplayName("GestorPrestecs")
class GestorPrestecsTest {
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);
@Mock private MaterialRepository materials;
@Mock private EmpleatRepository empleats;
@Mock private PrestecRepository prestecs;
@Mock private ServeiAvisos avisos;
private GestorPrestecs gestor;
private Material llibre;
private Empleat marta;
@BeforeEach
void preparar() {
// Propietats i calculadora REALS: son barates, deterministes i sense efectes
var propietats = new PropietatsBiblioTech(
"BiblioTech",
new PropietatsBiblioTech.Prestec(15, 3),
new PropietatsBiblioTech.Multa(new BigDecimal("0.50"), new BigDecimal("20.00")),
new PropietatsBiblioTech.Reserva(3));
var calculadora = new CalculadoraMultes(propietats, RELLOTGE);
gestor = new GestorPrestecs(materials, empleats, prestecs, calculadora,
avisos, propietats, RELLOTGE);
llibre = new Llibre("978-0000000001", "Java Eficac", 3, "J. Bloch");
marta = new Empleat("[email protected]", "Marta Ruiz");
}
@Nested
@DisplayName("en prestar")
class Prestar {
@Test
@DisplayName("crea un préstec actiu amb venciment a 15 dies")
void creaPrestecCorrecte() {
when(materials.findByIsbn("978-0000000001")).thenReturn(Optional.of(llibre));
when(empleats.findByCorreu(marta.getCorreu())).thenReturn(Optional.of(marta));
when(prestecs.save(any(Prestec.class))).thenAnswer(inv -> inv.getArgument(0));
Prestec prestec = gestor.prestar("978-0000000001", marta.getCorreu());
assertThat(prestec.getMaterial()).isEqualTo(llibre);
assertThat(prestec.getEmpleat()).isEqualTo(marta);
assertThat(prestec.getEstat()).isEqualTo(EstatPrestec.ACTIU);
assertThat(prestec.getDataPrestec()).isEqualTo(AVUI);
assertThat(prestec.getDataVenciment()).isEqualTo(AVUI.plusDays(15));
}
@Test
@DisplayName("decrementa els exemplars disponibles del material")
void decrementaExemplars() {
when(materials.findByIsbn("978-0000000001")).thenReturn(Optional.of(llibre));
when(empleats.findByCorreu(marta.getCorreu())).thenReturn(Optional.of(marta));
when(prestecs.save(any(Prestec.class))).thenAnswer(inv -> inv.getArgument(0));
gestor.prestar("978-0000000001", marta.getCorreu());
assertThat(llibre.getExemplarsDisponibles()).isEqualTo(2); // estat real
}
@Test
@DisplayName("falla si el material no existeix, i no desa res")
void materialInexistent() {
when(materials.findByIsbn("978-9999999999")).thenReturn(Optional.empty());
assertThatThrownBy(() -> gestor.prestar("978-9999999999", marta.getCorreu()))
.isInstanceOf(MaterialNoTrobatException.class)
.hasMessageContaining("978-9999999999");
verify(prestecs, never()).save(any());
verifyNoInteractions(avisos);
}
@Test
@DisplayName("falla si el material no té exemplars disponibles")
void senseExemplars() {
Material exhaurit = new Llibre("978-0000000003", "Refactoritzacio", 0, "M. Fowler");
when(materials.findByIsbn("978-0000000003")).thenReturn(Optional.of(exhaurit));
when(empleats.findByCorreu(marta.getCorreu())).thenReturn(Optional.of(marta));
assertThatThrownBy(() -> gestor.prestar("978-0000000003", marta.getCorreu()))
.isInstanceOf(SenseExemplarsException.class);
verify(prestecs, never()).save(any());
}
@Test
@DisplayName("falla si l'empleat ha assolit el límit de 3 préstecs")
void limitAssolit() {
when(materials.findByIsbn("978-0000000001")).thenReturn(Optional.of(llibre));
when(empleats.findByCorreu(marta.getCorreu())).thenReturn(Optional.of(marta));
when(prestecs.countByEmpleatAndEstat(marta, EstatPrestec.ACTIU))
.thenReturn(3L);
assertThatThrownBy(() -> gestor.prestar("978-0000000001", marta.getCorreu()))
.isInstanceOf(LimitPrestecsException.class)
.hasMessageContaining(marta.getCorreu());
verify(prestecs, never()).save(any());
assertThat(llibre.getExemplarsDisponibles()).isEqualTo(3); // sense efectes
}
}
@Nested
@DisplayName("davant de fallades d'infraestructura")
class Fallades {
@Test
@DisplayName("si el desat falla, no s'avisa ningú")
void fallaElDesat() {
when(materials.findByIsbn("978-0000000001")).thenReturn(Optional.of(llibre));
when(empleats.findByCorreu(marta.getCorreu())).thenReturn(Optional.of(marta));
when(prestecs.save(any()))
.thenThrow(new DataAccessResourceFailureException("Connexio perduda"));
assertThatThrownBy(() -> gestor.prestar("978-0000000001", marta.getCorreu()))
.isInstanceOf(DataAccessResourceFailureException.class);
verifyNoInteractions(avisos); // requisit: no es notifica el no persistit
}
@Test
@DisplayName("reintenta davant d'un conflicte de concurrència i acaba amb èxit")
void reintentaDavantConflicte() {
when(materials.findByIsbn("978-0000000001")).thenReturn(Optional.of(llibre));
when(empleats.findByCorreu(marta.getCorreu())).thenReturn(Optional.of(marta));
when(prestecs.save(any()))
.thenThrow(new OptimisticLockingFailureException("versio obsoleta"))
.thenAnswer(inv -> inv.getArgument(0)); // segona vegada, exit
Resultat<Prestec> resultat =
gestor.prestarAmbReintent("978-0000000001", marta.getCorreu());
assertThat(resultat.esExit()).isTrue();
verify(prestecs, times(2)).save(any()); // es va intentar dues vegades
}
}
}Decisions que convé subratllar:
CalculadoraMultesiPropietatsBiblioTechsón reals. Són barates, deterministes i sense efectes: simular-les només afegiria soroll i debilitaria la prova.- Mai no es verifica
findByIsbnnifindByCorreu. Són consultes. Que es van cridar ja és implícit en el fet que el resultat és correcte. - Sí que es verifica
saveiavisar. Són ordres amb efecte. never()iverifyNoInteractionsals casos de fallada. Comprovar absències és onverifyaporta valor únic.- La prova del reintent només és possible amb
thenThrow().thenAnswer(). És l'escenari del bloqueig optimista d'11-03, impossible de provocar d'una altra forma.
- BiblioTech:
ServeiAvisos verificat amb ArgumentCaptor
ServeiAvisos verificat amb ArgumentCaptor@ExtendWith(MockitoExtension.class)
@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);
@Mock private PrestecRepository prestecs;
@Mock private ServeiAvisos avisos;
@Captor private ArgumentCaptor<Empleat> capturadorEmpleat;
@Captor private ArgumentCaptor<String> capturadorMissatge;
private ServeiAvisosVenciment servei;
@BeforeEach
void preparar() {
servei = new ServeiAvisosVenciment(prestecs, avisos, propietatsAmbAntelacio(2), RELLOTGE);
}
@Test
@DisplayName("el missatge inclou el títol del material i els dies restants")
void contingutDelMissatge() {
when(prestecs.cercarActius()).thenReturn(List.of(
prestec("Java Eficac", marta(), AVUI.plusDays(2))));
servei.enviarAvisosDelDia();
verify(avisos).avisar(capturadorEmpleat.capture(), capturadorMissatge.capture());
assertThat(capturadorEmpleat.getValue().getCorreu())
.isEqualTo("[email protected]");
assertThat(capturadorMissatge.getValue())
.contains("Java Eficac")
.contains("2 dies")
.doesNotContain("null") // bug classic de concatenacio
.doesNotContain("@{"); // plantilla sense substituir
}
@Test
@DisplayName("avisa només els empleats el préstec dels quals venç en exactament 2 dies")
void finestraDAvis() {
when(prestecs.cercarActius()).thenReturn(List.of(
prestec("Java Eficac", marta(), AVUI.plusDays(2)), // SI
prestec("Patrons de Disseny", diego(), AVUI.plusDays(2)), // SI
prestec("Refactoritzacio", nuria(), AVUI.plusDays(1)), // no
prestec("Java Eficac", nuria(), AVUI.plusDays(3)), // no
prestec("Refactoritzacio", diego(), AVUI.minusDays(5))));// no
servei.enviarAvisosDelDia();
verify(avisos, times(2)).avisar(capturadorEmpleat.capture(), anyString());
assertThat(capturadorEmpleat.getAllValues())
.extracting(Empleat::getCorreu)
.containsExactly("[email protected]",
"[email protected]");
}
@Test
@DisplayName("un canal caigut no impedeix els avisos restants")
void unaFalladaNoAturaLaResta() {
when(prestecs.cercarActius()).thenReturn(List.of(
prestec("Java Eficac", marta(), AVUI.plusDays(2)),
prestec("Patrons de Disseny", diego(), AVUI.plusDays(2)),
prestec("Refactoritzacio", nuria(), AVUI.plusDays(2))));
// El segon avis falla
doNothing()
.doThrow(new ServeiAvisosException("SMTP no respon"))
.doNothing()
.when(avisos).avisar(any(), anyString());
int enviats = servei.enviarAvisosDelDia();
assertThat(enviats).isEqualTo(2); // 3 intents, 2 amb exit
verify(avisos, times(3)).avisar(any(), anyString()); // es van intentar els 3
}
}Aquesta última prova és un exemple excel·lent del que només Mockito permet: encadenar doNothing().doThrow().doNothing() per simular que la segona de tres crides falla. Verifica l'estratègia de resiliència del mòdul 6 —una fallada parcial no ha d'avortar el procés complet— i provocar-la d'una altra forma seria inviable.
- BiblioTech:
ClientMetadades provat sense xarxa
ClientMetadades provat sense xarxaEl ClientMetadades de 09-06 crida una API externa amb HttpClient. Provar-lo tal qual significa dependre de la xarxa.
Primer, el disseny correcte (recorda l'apartat 21: no simulis HttpClient directament):
package com.nexussoftware.bibliotech.xarxa;
// Interficie PROPIA que expressa el que el domini necessita
public interface PassarelaMetadades {
Optional<MetadadesLlibre> cercarPerIsbn(String isbn);
}
// Implementacio real, que si que fa servir HttpClient
@Component
public class PassarelaMetadadesHttp implements PassarelaMetadades {
private final HttpClient http;
private final String urlBase;
public PassarelaMetadadesHttp(HttpClient http,
@Value("${bibliotech.metadades.url}") String urlBase) {
this.http = http;
this.urlBase = urlBase;
}
@Override
public Optional<MetadadesLlibre> cercarPerIsbn(String isbn) { /* HTTP real */ }
}I el servei que la fa servir:
@Service
public class EnriquidorCataleg {
private final PassarelaMetadades passarela; // la INTERFICIE, no HttpClient
private final MaterialRepository materials;
public EnriquidorCataleg(PassarelaMetadades passarela, MaterialRepository materials) {
this.passarela = passarela;
this.materials = materials;
}
public int enriquirCataleg() {
int enriquits = 0;
for (Material material : materials.findByAutorIsNull()) {
try {
Optional<MetadadesLlibre> metadades = passarela.cercarPerIsbn(material.getIsbn());
if (metadades.isPresent()) {
material.completarAmb(metadades.get());
materials.save(material);
enriquits++;
}
} catch (PassarelaNoDisponibleException e) {
log.warn("No s'ha pogut enriquir {}: {}", material.getIsbn(), e.getMessage());
// continua amb el seguent
}
}
return enriquits;
}
}Ara la prova, sense una sola connexió de xarxa:
@ExtendWith(MockitoExtension.class)
@DisplayName("Enriquiment del catàleg")
class EnriquidorCatalegTest {
@Mock private PassarelaMetadades passarela;
@Mock private MaterialRepository materials;
private EnriquidorCataleg enriquidor;
@BeforeEach
void preparar() { enriquidor = new EnriquidorCataleg(passarela, materials); }
@Test
@DisplayName("completa l'autor dels materials que el tenen buit")
void completaLesDades() {
Material senseAutor = new Llibre("978-0000000001", "Java Eficac", 3, null);
when(materials.findByAutorIsNull()).thenReturn(List.of(senseAutor));
when(passarela.cercarPerIsbn("978-0000000001"))
.thenReturn(Optional.of(new MetadadesLlibre("Joshua Bloch", 2018, "Addison-Wesley")));
int enriquits = enriquidor.enriquirCataleg();
assertThat(enriquits).isEqualTo(1);
assertThat(senseAutor.getAutor()).isEqualTo("Joshua Bloch");
verify(materials).save(senseAutor);
}
@Test
@DisplayName("no desa res si l'API no coneix l'ISBN")
void isbnDesconegut() {
Material senseAutor = new Llibre("978-0000000099", "Titol rar", 1, null);
when(materials.findByAutorIsNull()).thenReturn(List.of(senseAutor));
when(passarela.cercarPerIsbn(anyString())).thenReturn(Optional.empty());
assertThat(enriquidor.enriquirCataleg()).isZero();
verify(materials, never()).save(any());
}
@Test
@DisplayName("si l'API està caiguda, continua amb la resta del catàleg")
void apiCaigudaParcialment() {
Material un = new Llibre("978-0000000001", "Java Eficac", 3, null);
Material dos = new Llibre("978-0000000002", "Patrons de Disseny", 2, null);
when(materials.findByAutorIsNull()).thenReturn(List.of(un, dos));
when(passarela.cercarPerIsbn("978-0000000001"))
.thenThrow(new PassarelaNoDisponibleException("504 Gateway Timeout"));
when(passarela.cercarPerIsbn("978-0000000002"))
.thenReturn(Optional.of(new MetadadesLlibre("GoF", 1994, "Addison-Wesley")));
assertThat(enriquidor.enriquirCataleg()).isEqualTo(1);
assertThat(dos.getAutor()).isEqualTo("GoF");
verify(materials, times(1)).save(dos);
}
@Test
@DisplayName("no crida l'API si no hi ha materials incomplets")
void catalegComplet() {
when(materials.findByAutorIsNull()).thenReturn(List.of());
assertThat(enriquidor.enriquirCataleg()).isZero();
verifyNoInteractions(passarela); // ni una peticio innecessaria
}
}Quatre proves, mil·lisegons, sense xarxa, i cobrint escenaris —l'API caiguda, un ISBN desconegut, una fallada parcial— que amb l'API real serien impredictibles o impossibles.
I per provar PassarelaMetadadesHttp en si mateixa, que és la classe que parla HTTP, l'eina correcta no és Mockito sinó WireMock: un servidor HTTP fals que respon el que li programis. Això és una prova d'integració i pertany a 12-05.
- Proves d'integració:
@SpringBootTest i @MockitoBean
@SpringBootTest i @MockitoBeanLes proves unitàries no ho cobreixen tot. Falta comprovar que les peces encaixen: que Spring injecta el correcte, que la configuració es llegeix, que les transaccions funcionen.
@SpringBootTest
@ActiveProfiles("test")
class GestorPrestecsIT { // sufix IT: l'executa failsafe (11-05)
@Autowired private GestorPrestecs gestor;
@Autowired private MaterialRepository materials;
// Substitueix el bean real al context de Spring per un mock
@MockitoBean private ServeiAvisos avisos;
@Test
void prestarPersisteixIAvisa() {
materials.save(new Llibre("978-0000000001", "Java Eficac", 3, "J. Bloch"));
Prestec prestec = gestor.prestar("978-0000000001", "[email protected]");
assertThat(prestec.getId()).isNotNull(); // es va persistir de veritat
verify(avisos).avisar(any(), contains("Java Eficac")); // no es va enviar correu real
}
}Sobre el nom de l'anotació: @MockitoBean és la de Spring Boot 3.4 endavant; en versions anteriors era @MockBean, avui obsoleta. Fan el mateix: reemplaçar un bean del context per un mock.
Diferència clau amb @Mock:
@Mock |
@MockitoBean |
|
|---|---|---|
| Àmbit | Un objecte a la prova | Un bean del context de Spring |
| Necessita context | No | Sí |
| Velocitat | Mil·lisegons | Segons |
| Ús | Proves unitàries | Proves d'integració |
Avís de rendiment poc conegut: cada combinació diferent de @MockitoBean crea un context de Spring nou. Spring desa els contextos en memòria cau entre classes de prova, però si cada classe simula beans diferents, s'arrenquen molts contextos i el conjunt es torna lentíssim. És una de les causes més freqüents de "les nostres proves triguen quinze minuts".
@DataJpaTest amb H2
@DataJpaTest amb H2Per provar la capa de persistència sense arrencar tota l'aplicació:
@DataJpaTest // nomes JPA: repositoris, EntityManager, BD incrustada
@Tag("integracio")
class PrestecRepositoryIT {
@Autowired private PrestecRepository repositori;
@Autowired private TestEntityManager em;
@Test
@DisplayName("troba els préstecs vençuts sense retornar, amb les relacions carregades")
void trobaVencuts() {
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.persist(new Prestec(llibre, marta, LocalDate.of(2026, 3, 1), LocalDate.of(2026, 4, 10)));
em.flush();
em.clear(); // buida el context: forca a llegir de la BD de veritat
List<Prestec> vencuts = repositori.vencuts(LocalDate.of(2026, 3, 20));
assertThat(vencuts).hasSize(1);
// El JOIN FETCH d'11-03 funciona: no hi ha LazyInitializationException
assertThat(vencuts.get(0).getMaterial().getTitol()).isEqualTo("Java Eficac");
}
}Aquest em.clear() és un detall que molta gent oblida i que fa la diferència: sense ell, les entitats continuen al context de persistència de primer nivell (11-03) i la consulta les retorna de la memòria cau sense tocar la base de dades. La prova passaria encara que el mapatge fos incorrecte.
I aquesta prova verifica una cosa que una prova unitària no pot: que el JPQL és sintàcticament vàlid, que els noms d'atribut existeixen, que el JOIN FETCH evita l'N+1 i que el mapatge genera l'esquema correcte. Tot això només es comprova contra una base de dades real.
Característiques de @DataJpaTest:
| Característica | Comportament |
|---|---|
| Context | Només la capa JPA (repositoris, entitats, DataSource) |
| Base de dades | Incrustada (H2) per defecte |
| Transacció | Una per prova, amb reversió automàtica en acabar |
TestEntityManager |
Versió simplificada de l'EntityManager per preparar dades |
| Serveis i controladors | No es carreguen |
La reversió automàtica és el que fa que les proves siguin independents: cadascuna parteix d'una base de dades neta sense que l'hagis de netejar.
- Testcontainers, presentat
H2 és còmode per aprendre i per al cicle ràpid, però no és la base de dades de producció. Les seves diferències amb PostgreSQL o MySQL són reals: dialectes SQL diferents, comportament de les seqüències, tipus, funcions, nivells d'aïllament per defecte. Una consulta nativa que funciona a H2 pot fallar a PostgreSQL.
Testcontainers és la resposta estàndard avui: arrenca la base de dades real en un contenidor Docker, durant la prova.
@SpringBootTest
@Testcontainers
class PrestecRepositoryPostgresIT {
@Container
@ServiceConnection // Spring Boot 3.1+: configura el DataSource sol
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:16-alpine");
@Autowired private PrestecRepository repositori;
@Test
void funcionaContraPostgresDeVeritat() {
// S'executa contra PostgreSQL 16 real, arrencat i destruit per la prova
}
}| H2 en memòria | Testcontainers | |
|---|---|---|
| Velocitat | Mil·lisegons | Segons (l'arrencada es comparteix) |
| Fidelitat | Aproximada | Idèntica a producció |
| Requisits | Cap | Docker |
| Quan | Cicle ràpid, aprenentatge | Integració contínua, consultes natives |
Serveix també per a cues de missatges, Redis, Elasticsearch o qualsevol servei amb imatge Docker. Es desenvolupa a 12-05.
- Cobertura: interpretació honesta
La cobertura mesura quin percentatge del codi executen les proves. Amb Mockito és fàcil inflar-la sense provar res:
@Test
void coberturaAlta() {
when(materials.findByIsbn(anyString())).thenReturn(Optional.of(llibre));
when(empleats.findByCorreu(anyString())).thenReturn(Optional.of(marta));
when(prestecs.save(any())).thenReturn(prestec);
gestor.prestar("978-0000000001", "[email protected]");
// Ni una asercio. Cobertura del metode: 100 %. Valor: zero.
}Una línia executada no és una línia provada.
El que la cobertura sí que diu:
- 0 % en un mètode de negoci és informació valuosa: ningú l'ha provat.
- Una branca sense cobrir assenyala un camí que mai no s'exercita, sovint el d'error.
El que no diu: res sobre si les assercions comproven el correcte, si els casos límit estan coberts o si el comportament és l'esperat.
I hi ha un efecte pervers conegut: un objectiu de cobertura alt genera proves dolentes. Si l'equip ha d'arribar al 90 %, apareixeran proves de getters i setters que pugen el número sense aportar res, i ningú escriurà la prova difícil del cas límit perquè costa més.
Es desenvolupa a 12-05, amb JaCoCo configurat i criteris raonables.
- Errors comuns i consells
Error: barrejar matchers i valors literals. verify(avisos).avisar(marta, anyString()) llança InvalidUseOfMatchersException. O tots matchers, o cap: eq(marta).
Error: fer servir when amb un mètode void o amb un espia. No compila amb void, i amb un espia executa el mètode real. Fes servir doThrow / doReturn.
Error: verificar consultes. verify(materials).findByIsbn(...) prova implementació i produeix proves fràgils. Verifica ordres.
Error: simular tipus que no controles. Simules la teva creença sobre com es comporten. Embolcalla'ls en una interfície pròpia.
Error: simular classes de valor. String, LocalDate, BigDecimal i els teus record es construeixen: són barats i el seu comportament és el que vols.
Error: verifyNoMoreInteractions per sistema. Qualsevol crida nova i legítima trenca la prova.
Error: @InjectMocks amb dependències no simulables. Injecta null sense avisar i la fallada apareix més tard com a NullPointerException. Construeix explícitament.
Error: no tancar mockStatic. Contamina les proves següents. try-with-resources sempre — i abans, pregunta't si el disseny hauria de canviar.
Error: proves sense cap asserció. Amb Mockito és fàcil escriure una prova que només configura i executa. Cobertura alta, valor zero.
Error: silenciar UnnecessaryStubbingException. Està assenyalant un comportament programat que ningú fa servir: o la prova no fa el que creus, o hi ha codi mort.
Error: fer servir @SpringBootTest amb molts @MockitoBean diferents. Cada combinació crea un context nou i el conjunt es torna lentíssim.
Consell: aplica la regla de les ordres i les consultes. És l'única cosa que necessites recordar per escriure proves amb Mockito molt millors que la mitjana.
Consell: fes servir objectes reals per al que és barat. Un record de configuració, una calculadora pura o un Clock.fixed no es simulen: es fan servir. La prova és més forta i més llegible.
Consell: considera un fake en memòria per al repositori principal. S'escriu una vegada, dóna coherència d'estat gratis i no es trenca en refactoritzar.
Consell: ArgumentCaptor per a comprovacions no trivials. Els seus missatges d'error són molt millors que els d'argThat.
Consell: aprofita thenThrow per als camins d'error. És la capacitat més valuosa de Mockito i la que cobreix el codi que en producció s'executa el pitjor dia.
Consell: abans d'escriure un mock complicat, pregunta't si el disseny hauria de canviar. El Clock de 10-05 va eliminar la necessitat d'un mock estàtic. Aquesta és la millor solució possible.
Consell: fes servir l'anotació de prova més petita que serveixi. Sense Spring si pot ser; @DataJpaTest si necessites la base de dades; @SpringBootTest només quan de veritat calgui el context complet.
- Exercicis
Exercici 1: triar el doble adequat
Per a cada escenari de BiblioTech, indica quin tipus de doble faries servir (dummy, stub, spy, mock, fake, o cap) i per què. Escriu a més el fragment de codi de la part rellevant.
- Provar que
CalculadoraMulteslimita la multa a 20 € quan el retard és de 200 dies. - Provar que en prestar l'últim exemplar s'envia un avís al bibliotecari responsable.
- Provar que
GestorPrestecsno desa res si l'empleat ha assolit el límit. - Provar
EstadistiquesBiblioTech, que fa vuit consultes diferents al repositori i agrega els resultats amb streams. - Provar que
EnriquidorCatalegcontinua processant quan l'API externa retorna un error 503. - Provar que
RegistreOperacionsescriu una entrada d'auditoria per cada préstec. - Provar que
ProcessadorReservescaduca les reserves de més de 3 dies.
Exercici 2: arreglar una prova fràgil
Aquesta prova és al projecte de Nexus Software. Passa, però és dolenta. Identifica almenys sis problemes, explica per què cadascun ho és, i reescriu-la.
@ExtendWith(MockitoExtension.class)
class ProcessadorReservesTest {
@Mock private ReservaRepository reserves;
@Mock private GestorPrestecs gestor;
@Mock private PropietatsBiblioTech propietats;
@Mock private PropietatsBiblioTech.Reserva propietatsReserva;
@Mock private Clock rellotge;
@Mock private Reserva reserva;
@Mock private Material material;
@InjectMocks private ProcessadorReserves processador;
@Test
void test() {
when(propietats.reserva()).thenReturn(propietatsReserva);
when(propietatsReserva.diesCaducitat()).thenReturn(3);
when(rellotge.instant()).thenReturn(Instant.parse("2026-03-20T10:00:00Z"));
when(rellotge.getZone()).thenReturn(ZoneId.of("Europe/Madrid"));
when(reserves.pendents()).thenReturn(List.of(reserva));
when(reserva.getDataSolicitud()).thenReturn(LocalDate.of(2026, 3, 10));
when(reserva.getMaterial()).thenReturn(material);
when(material.getIsbn()).thenReturn("978-0000000001");
when(gestor.hiHaDisponible(anyString())).thenReturn(false);
processador.processarPendents();
verify(propietats).reserva();
verify(propietatsReserva).diesCaducitat();
verify(reserves).pendents();
verify(reserva).getDataSolicitud();
verify(reserves).caducar(reserva);
verifyNoMoreInteractions(reserves, gestor, propietats);
}
}Exercici 3: provar el flux complet de devolució
Implementa les proves d'aquest mètode, que és el més complex de BiblioTech:
@Service
public class GestorDevolucions {
private final PrestecRepository prestecs;
private final CalculadoraMultes calculadora;
private final RegistreMultes multes;
private final ProcessadorReserves reserves;
private final ServeiAvisos avisos;
private final Clock rellotge;
@Transactional
public ResultatDevolucio retornar(Long prestecId) {
Prestec prestec = prestecs.findById(prestecId)
.orElseThrow(() -> new PrestecNoTrobatException(prestecId));
if (prestec.getDataDevolucio().isPresent()) {
throw new PrestecJaRetornatException(prestecId);
}
LocalDate avui = LocalDate.now(rellotge);
BigDecimal multa = calculadora.calcular(prestec);
prestec.retornar(avui);
prestec.getMaterial().retornarUnExemplar();
prestecs.save(prestec);
if (multa.compareTo(BigDecimal.ZERO) > 0) {
multes.registrar(prestec, multa);
avisos.avisar(prestec.getEmpleat(),
"S'ha registrat una multa de " + multa + " EUR per la devolucio tardana de "
+ prestec.getMaterial().getTitol());
}
Optional<Reserva> seguent = reserves.seguentDe(prestec.getMaterial());
seguent.ifPresent(r -> {
r.avisar(rellotge);
avisos.avisar(r.getEmpleat(),
"Ja esta disponible el material que vas reservar: "
+ prestec.getMaterial().getTitol());
});
return new ResultatDevolucio(prestec, multa, seguent.isPresent());
}
}Escriu una classe de prova que cobreixi, aplicant el criteri de la lliçó:
- Devolució en termini: sense multa, sense avís de multa.
- Devolució amb 10 dies de retard: multa de 5,00 € registrada i avisada, amb el contingut del missatge verificat amb
ArgumentCaptor. - Devolució d'un material amb una reserva pendent: s'avisa també el següent empleat (dos avisos en total, comprovats en ordre i contingut).
- Préstec inexistent: excepció i cap efecte.
- Préstec ja retornat: excepció i cap efecte.
- El material recupera un exemplar disponible.
Justifica en cada cas què simules, què fas servir real i què verifiques.
Solucions
Solució 1
| # | Escenari | Doble | Per què |
|---|---|---|---|
| 1 | Límit de la multa | Cap | CalculadoraMultes només necessita PropietatsBiblioTech (un record) i un Clock.fixed. Tots dos reals. És el cas ideal: el disseny va fer innecessari el doble |
| 2 | Avís al bibliotecari | Mock de ServeiAvisos |
Enviar un avís és una ordre amb efecte extern. L'única forma de comprovar-ho és verify |
| 3 | Límit de préstecs | Stub de repositori + mock per verificar l'absència | countByEmpleatAndEstat és consulta (stub); verify(prestecs, never()).save(any()) verifica que no hi va haver ordre |
| 4 | EstadistiquesBiblioTech |
Fake en memòria | Vuit consultes diferents serien vuit when per prova, i els resultats han de ser coherents entre si (un préstec que apareix en una consulta ha d'aparèixer en una altra). Un fake dóna aquesta coherència gratis |
| 5 | API que retorna 503 | Mock amb thenThrow |
És la capacitat exclusiva de Mockito: provocar una fallada impossible de reproduir d'una altra forma |
| 6 | Auditoria per préstec | Mock + ArgumentCaptor |
anotar és ordre; i cal comprovar el contingut de l'entrada, no només que es va cridar |
| 7 | Caducar reserves | Fake o stub + Clock.fixed |
La clau és el rellotge fix (no un mock de Clock, sinó Clock.fixed); el repositori pot ser un fake que a més permeti comprovar l'estat final |
Fragments rellevants:
// 1. Sense dobles: tot real
var calculadora = new CalculadoraMultes(propietatsDeProva(), Clock.fixed(...));
assertThat(calculadora.calcular(prestecAmbRetard(200))).isEqualByComparingTo("20.00");// 2. Mock per verificar l'ordre
verify(avisos).avisar(eq(bibliotecariResponsable), contains("ultim exemplar"));// 3. Stub per a la consulta, verify(never) per a l'absencia d'ordre
when(prestecs.countByEmpleatAndEstat(marta, ACTIU)).thenReturn(3L);
assertThatThrownBy(() -> gestor.prestar(...)).isInstanceOf(LimitPrestecsException.class);
verify(prestecs, never()).save(any());// 4. Fake: coherencia entre consultes, gratis
var repositori = new RepositoriPrestecsEnMemoria();
repositori.save(prestecDe(marta, "978-0000000001"));
repositori.save(prestecDe(marta, "978-0000000002"));
repositori.save(prestecDe(diego, "978-0000000001"));
var estadistiques = new EstadistiquesBiblioTech(repositori);
assertThat(estadistiques.materialMesPrestat()).contains("978-0000000001");
assertThat(estadistiques.prestecsPerEmpleat()).containsEntry("Marta Ruiz", 2L);// 5. thenThrow: l'escenari impossible de reproduir
when(passarela.cercarPerIsbn("978-0000000001"))
.thenThrow(new PassarelaNoDisponibleException("503 Service Unavailable"));// 6. Captor per al contingut de l'auditoria
verify(registre).anotar(captorEntrada.capture());
assertThat(captorEntrada.getValue())
.extracting(EntradaAuditoria::operacio, EntradaAuditoria::isbn)
.containsExactly("PRESTEC", "978-0000000001");// 7. Rellotge fix REAL, no mock de Clock
var processador = new ProcessadorReserves(repositori, gestor, propietats,
Clock.fixed(Instant.parse("2026-03-20T10:00:00Z"), MADRID));Solució 2
Els problemes:
| # | Problema | Per què és dolent |
|---|---|---|
| 1 | @Mock sobre Clock |
Clock és una classe de valor amb Clock.fixed. Simular-la obliga a programar instant() i getZone(), és més fràgil i menys llegible. I si el codi crida un altre mètode del rellotge, retorna null |
| 2 | @Mock sobre PropietatsBiblioTech i el seu record imbricat |
Són record immutables: es construeixen amb new en una línia. Simular-los genera quatre línies de configuració inútils |
| 3 | @Mock sobre Reserva i Material |
Són entitats de domini. Simular-les fa que la prova no exerciti la seva lògica real (validacions, transicions d'estat) i produeix el "mock de mock de mock" |
| 4 | Verificar consultes (propietats.reserva(), diesCaducitat(), pendents(), getDataSolicitud()) |
És implementació pura. Qualsevol refactorització —desar el valor de configuració en memòria cau, per exemple— trenca la prova sense que canviï el comportament |
| 5 | verifyNoMoreInteractions sobre tres mocks |
Afegir una traça, una mètrica o una comprovació addicional trenca la prova |
| 6 | Nom test |
No diu res. Quan falla a la integració contínua, no se sap què s'ha trencat |
| 7 | Cap asserció sobre el resultat o l'estat | Només verifica crides. Si caducar rebés la reserva equivocada... bé, això sí que es veuria; però si l'estat de la reserva no canviés, la prova continuaria verda |
| 8 | @InjectMocks |
Amb tants mocks funciona, però si demà s'afegeix un paràmetre no simulable al constructor, injecta null sense avisar |
Versió reescrita:
@ExtendWith(MockitoExtension.class)
@DisplayName("ProcessadorReserves")
class ProcessadorReservesTest {
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);
@Mock private ReservaRepository reserves; // col.laborador extern: mock
@Mock private GestorPrestecs gestor; // col.laborador amb efectes: mock
private ProcessadorReserves processador;
private Material llibre;
private Empleat marta;
@BeforeEach
void preparar() {
// Objectes REALS: records de configuracio, rellotge fix i entitats de domini
var propietats = new PropietatsBiblioTech(
"BiblioTech",
new PropietatsBiblioTech.Prestec(15, 3),
new PropietatsBiblioTech.Multa(new BigDecimal("0.50"), new BigDecimal("20.00")),
new PropietatsBiblioTech.Reserva(3)); // 3 dies de caducitat
// Construccio explicita, no @InjectMocks
processador = new ProcessadorReserves(reserves, gestor, propietats, RELLOTGE);
llibre = new Llibre("978-0000000001", "Java Eficac", 0, "J. Bloch");
marta = new Empleat("[email protected]", "Marta Ruiz");
}
@Test
@DisplayName("caduca les reserves sol·licitades fa més de 3 dies")
void caducaLesReservesAntigues() {
// Sol.licitada el 10 -> caducava el 13 -> avui es 20: caducada
Reserva antiga = new Reserva(llibre, marta, LocalDate.of(2026, 3, 10), 3);
when(reserves.pendents()).thenReturn(List.of(antiga));
processador.processarPendents();
verify(reserves).caducar(antiga); // ORDRE: es verifica
assertThat(antiga.getEstat()).isEqualTo(EstatReserva.CADUCADA); // ESTAT real
verify(gestor, never()).prestar(anyString(), anyString());
}
@Test
@DisplayName("no caduca una reserva dins del termini de 3 dies")
void noCaducaLesRecents() {
// Sol.licitada el 18 -> caduca el 21 -> avui es 20: encara vigent
Reserva recent = new Reserva(llibre, marta, LocalDate.of(2026, 3, 18), 3);
when(reserves.pendents()).thenReturn(List.of(recent));
when(gestor.hiHaDisponible("978-0000000001")).thenReturn(false);
processador.processarPendents();
verify(reserves, never()).caducar(any());
assertThat(recent.getEstat()).isEqualTo(EstatReserva.PENDENT);
}
@Test
@DisplayName("presta i completa la reserva quan hi ha exemplar disponible")
void completaLaReservaSiHiHaExemplar() {
Reserva vigent = new Reserva(llibre, marta, LocalDate.of(2026, 3, 18), 3);
when(reserves.pendents()).thenReturn(List.of(vigent));
when(gestor.hiHaDisponible("978-0000000001")).thenReturn(true);
processador.processarPendents();
verify(gestor).prestar("978-0000000001", marta.getCorreu()); // ORDRE
verify(reserves).completar(vigent); // ORDRE
verify(reserves, never()).caducar(any());
}
@Test
@DisplayName("el dia exacte de caducitat la reserva encara és vàlida")
void limitDelTermini() {
// Sol.licitada el 17 -> caduca el 20 -> avui es 20: ES l'ultim dia, encara val
Reserva alLimit = new Reserva(llibre, marta, LocalDate.of(2026, 3, 17), 3);
when(reserves.pendents()).thenReturn(List.of(alLimit));
when(gestor.hiHaDisponible(anyString())).thenReturn(false);
processador.processarPendents();
verify(reserves, never()).caducar(any());
}
@Test
@DisplayName("no fa res si no hi ha reserves pendents")
void senseReserves() {
when(reserves.pendents()).thenReturn(List.of());
processador.processarPendents();
verifyNoInteractions(gestor);
}
}Què ha canviat i per què:
| Abans | Ara | Benefici |
|---|---|---|
@Mock Clock amb dos when |
Clock.fixed real |
Dues línies menys, més llegible, sense risc de null |
@Mock sobre dos record de propietats |
Construïts amb new |
Quatre línies menys, i el valor és visible a la prova |
@Mock Reserva, @Mock Material |
Entitats reals | S'exercita la lògica de domini (getEstat, transicions) |
Cinc verify de consultes |
Cap | Sobreviu a les refactoritzacions |
verifyNoMoreInteractions |
never() i verifyNoInteractions puntuals |
Comprova el que importa sense bloquejar canvis legítims |
Un mètode test |
Cinc proves amb noms descriptius | Quan falla, se sap què |
| Sense assercions d'estat | assertThat(reserva.getEstat()) |
Comprova l'efecte real, no només la crida |
| Sense casos límit | El dia exacte de caducitat | Allà és on viu el bug de < enfront de <= |
Solució 3
package com.nexussoftware.bibliotech.servei;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.math.BigDecimal;
import java.time.*;
import java.util.*;
import org.junit.jupiter.api.*;
import org.mockito.*;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
@DisplayName("GestorDevolucions")
class GestorDevolucionsTest {
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);
// MOCKS: col.laboradors amb efectes externs o infraestructura
@Mock private PrestecRepository prestecs;
@Mock private RegistreMultes multes;
@Mock private ProcessadorReserves reserves;
@Mock private ServeiAvisos avisos;
@Captor private ArgumentCaptor<String> capturadorMissatge;
@Captor private ArgumentCaptor<Empleat> capturadorEmpleat;
@Captor private ArgumentCaptor<BigDecimal> capturadorQuantitat;
private GestorDevolucions gestor;
private Material llibre;
private Empleat marta;
private Empleat diego;
@BeforeEach
void preparar() {
var propietats = new PropietatsBiblioTech(
"BiblioTech",
new PropietatsBiblioTech.Prestec(15, 3),
new PropietatsBiblioTech.Multa(new BigDecimal("0.50"), new BigDecimal("20.00")),
new PropietatsBiblioTech.Reserva(3));
// REAL: pura, barata, determinista. Simular-la debilitaria la prova
var calculadora = new CalculadoraMultes(propietats, RELLOTGE);
gestor = new GestorDevolucions(prestecs, calculadora, multes, reserves, avisos, RELLOTGE);
llibre = new Llibre("978-0000000001", "Java Eficac", 2, "J. Bloch");
marta = new Empleat("[email protected]", "Marta Ruiz");
diego = new Empleat("[email protected]", "Diego Alonso");
}
// ============ (1) EN TERMINI ============
@Nested
@DisplayName("devolució en termini")
class EnTermini {
@Test
@DisplayName("no genera multa ni avís")
void senseMultaNiAvis() {
Prestec prestec = prestecQueVenc(AVUI.plusDays(5)); // encara en termini
when(prestecs.findById(1L)).thenReturn(Optional.of(prestec));
when(reserves.seguentDe(llibre)).thenReturn(Optional.empty());
ResultatDevolucio resultat = gestor.retornar(1L);
assertThat(resultat.multa()).isEqualByComparingTo("0.00");
assertThat(resultat.hiHaReservaPendent()).isFalse();
assertThat(prestec.getEstat()).isEqualTo(EstatPrestec.RETORNAT);
assertThat(prestec.getDataDevolucio()).contains(AVUI);
verify(prestecs).save(prestec); // ORDRE
verifyNoInteractions(multes); // no es va registrar multa
verify(avisos, never()).avisar(any(), anyString());
}
// ============ (6) EXEMPLARS ============
@Test
@DisplayName("el material recupera un exemplar disponible")
void recuperaExemplar() {
Prestec prestec = prestecQueVenc(AVUI.plusDays(5));
when(prestecs.findById(1L)).thenReturn(Optional.of(prestec));
when(reserves.seguentDe(llibre)).thenReturn(Optional.empty());
int abans = llibre.getExemplarsDisponibles();
gestor.retornar(1L);
assertThat(llibre.getExemplarsDisponibles()).isEqualTo(abans + 1);
}
}
// ============ (2) AMB RETARD ============
@Nested
@DisplayName("devolució amb retard")
class AmbRetard {
@Test
@DisplayName("registra una multa de 5,00 € per 10 dies i avisa amb el detall")
void multaDeDeuDies() {
Prestec prestec = prestecQueVenc(AVUI.minusDays(10));
when(prestecs.findById(1L)).thenReturn(Optional.of(prestec));
when(reserves.seguentDe(llibre)).thenReturn(Optional.empty());
ResultatDevolucio resultat = gestor.retornar(1L);
// La multa la calcula el codi REAL: 10 dies x 0,50 EUR = 5,00 EUR
assertThat(resultat.multa()).isEqualByComparingTo("5.00");
// ORDRE 1: registre de la multa, amb captura dels arguments reals
verify(multes).registrar(eq(prestec), capturadorQuantitat.capture());
assertThat(capturadorQuantitat.getValue()).isEqualByComparingTo("5.00");
// ORDRE 2: avis, amb el CONTINGUT verificat
verify(avisos).avisar(capturadorEmpleat.capture(), capturadorMissatge.capture());
assertThat(capturadorEmpleat.getValue()).isEqualTo(marta);
assertThat(capturadorMissatge.getValue())
.contains("multa")
.contains("5.00")
.contains("Java Eficac")
.doesNotContain("null");
}
@Test
@DisplayName("la multa es limita al màxim de 20 € amb retards molt llargs")
void multaLimitada() {
Prestec prestec = prestecQueVenc(AVUI.minusDays(300));
when(prestecs.findById(1L)).thenReturn(Optional.of(prestec));
when(reserves.seguentDe(llibre)).thenReturn(Optional.empty());
assertThat(gestor.retornar(1L).multa()).isEqualByComparingTo("20.00");
verify(multes).registrar(eq(prestec), argThat(m -> m.compareTo(new BigDecimal("20.00")) == 0));
}
}
// ============ (3) AMB RESERVA PENDENT ============
@Nested
@DisplayName("quan hi ha una reserva pendent del material")
class AmbReserva {
@Test
@DisplayName("avisa també el següent empleat de la cua")
void avisaAlSeguent() {
Prestec prestec = prestecQueVenc(AVUI.plusDays(5)); // en termini: nomes 1 avis
Reserva reservaDeDiego = new Reserva(llibre, diego, AVUI.minusDays(1), 3);
when(prestecs.findById(1L)).thenReturn(Optional.of(prestec));
when(reserves.seguentDe(llibre)).thenReturn(Optional.of(reservaDeDiego));
ResultatDevolucio resultat = gestor.retornar(1L);
assertThat(resultat.hiHaReservaPendent()).isTrue();
assertThat(reservaDeDiego.getEstat()).isEqualTo(EstatReserva.AVISADA);
assertThat(reservaDeDiego.getInstantAvis()).isPresent();
verify(avisos).avisar(capturadorEmpleat.capture(), capturadorMissatge.capture());
assertThat(capturadorEmpleat.getValue()).isEqualTo(diego);
assertThat(capturadorMissatge.getValue())
.contains("disponible")
.contains("Java Eficac");
}
@Test
@DisplayName("amb retard I reserva s'envien dos avisos, primer la multa")
void dosAvisosEnOrdre() {
Prestec prestec = prestecQueVenc(AVUI.minusDays(10));
Reserva reservaDeDiego = new Reserva(llibre, diego, AVUI.minusDays(1), 3);
when(prestecs.findById(1L)).thenReturn(Optional.of(prestec));
when(reserves.seguentDe(llibre)).thenReturn(Optional.of(reservaDeDiego));
gestor.retornar(1L);
verify(avisos, times(2))
.avisar(capturadorEmpleat.capture(), capturadorMissatge.capture());
// L'ordre aqui SI que es un requisit: l'afectat per la multa se n'assabenta primer
assertThat(capturadorEmpleat.getAllValues()).containsExactly(marta, diego);
assertThat(capturadorMissatge.getAllValues().get(0)).contains("multa");
assertThat(capturadorMissatge.getAllValues().get(1)).contains("disponible");
}
}
// ============ (4) i (5) CASOS D'ERROR ============
@Nested
@DisplayName("casos d'error")
class Errors {
@Test
@DisplayName("préstec inexistent: excepció i cap efecte")
void prestecInexistent() {
when(prestecs.findById(99L)).thenReturn(Optional.empty());
assertThatThrownBy(() -> gestor.retornar(99L))
.isInstanceOf(PrestecNoTrobatException.class)
.hasMessageContaining("99");
verify(prestecs, never()).save(any());
verifyNoInteractions(multes, avisos, reserves);
}
@Test
@DisplayName("préstec ja retornat: excepció i cap efecte")
void prestecJaRetornat() {
Prestec retornat = prestecQueVenc(AVUI.minusDays(10));
retornat.retornar(AVUI.minusDays(5)); // ja retornat
when(prestecs.findById(1L)).thenReturn(Optional.of(retornat));
int exemplarsAbans = llibre.getExemplarsDisponibles();
assertThatThrownBy(() -> gestor.retornar(1L))
.isInstanceOf(PrestecJaRetornatException.class);
verify(prestecs, never()).save(any());
verifyNoInteractions(multes, avisos, reserves);
// Cap efecte sobre l'estat del material
assertThat(llibre.getExemplarsDisponibles()).isEqualTo(exemplarsAbans);
}
}
// --- fabrica d'escenaris ---
private Prestec prestecQueVenc(LocalDate venciment) {
return new Prestec(llibre, marta, venciment.minusDays(15), venciment);
}
}Justificació de les decisions:
| Col·laborador | Decisió | Motiu |
|---|---|---|
PrestecRepository |
Mock | Infraestructura. findById és consulta (stub), save és ordre (verify) |
CalculadoraMultes |
REAL | Pura, barata, determinista. Fer-la servir de veritat fa que la prova comprovi que la multa val realment 5,00 €, no que es va passar el valor que el mock va retornar |
PropietatsBiblioTech |
REAL | És un record. Els valors són a la vista a la prova |
Clock |
Clock.fixed, no mock |
És una classe de valor amb fàbrica adequada. Simular-la seria pitjor en tots els sentits |
RegistreMultes |
Mock | Ordre amb efecte extern. Es verifica |
ServeiAvisos |
Mock + Captor | Ordre amb efecte extern, i cal comprovar el contingut del missatge |
ProcessadorReserves |
Mock | seguentDe és consulta (stub) |
Prestec, Material, Reserva, Empleat |
REALS | Entitats de domini amb lògica pròpia. Simular-les impediria comprovar transicions d'estat com getEstat() o getInstantAvis() |
I el criteri aplicat a cada prova:
- Mai no es verifica
findByIdniseguentDe: són consultes, i el seu efecte ja és implícit al resultat. - Sempre es verifica
save,registrariavisar: són ordres amb efecte observable. - Es comprova l'estat real de les entitats (
getEstat,getExemplarsDisponibles,getInstantAvis), no només les crides. - Als casos d'error es fa servir
verifyNoInteractionsper garantir la propietat més important del mètode: si falla, no ha de deixar efectes parcials. - L'ordre es verifica només en un cas, i perquè és un requisit de negoci explícit, no perquè el codi ho faci així.
- El cas del retard de 300 dies comprova el límit de la multa, amb el càlcul fet pel codi real.
Conclusió
Les proves de BiblioTech ja no s'aturen on acaben les classes fàcils.
Saps per què JUnit no n'hi ha prou: els col·laboradors reals són lents, no deterministes, difícils de posar en un estat concret o tenen efectes reals. I coneixes els cinc tipus de doble amb definicions precises —dummy, stub, spy, mock i fake— amb l'aclariment de vocabulari que evita confusions: a Mockito tot s'anomena mock, i la distinció és conceptual. I coneixes l'opció que gairebé tothom oblida: el fake en memòria, que s'escriu una vegada, dóna coherència d'estat gratis i no es trenca en refactoritzar — moltes vegades la millor inversió per al repositori principal d'una aplicació.
Domines Mockito 5: @ExtendWith(MockitoExtension.class) amb la seva validació estricta que t'avisa de comportaments programats i mai no usats; la creació de mocks; i per què construir l'objecte sota prova explícitament és millor que @InjectMocks, que injecta null en silenci, no admet objectes no simulables i amaga el senyal d'un constructor amb massa paràmetres. Programes comportament amb when(...).thenReturn(...) —sabent que no és màgia, sinó una invocació registrada—, provoques fallades impossibles de reproduir d'una altra forma amb thenThrow, i dónes respostes dinàmiques amb thenAnswer, amb l'avís que quan l'Answer té lògica, en realitat volies un fake.
Saps quan la sintaxi do... és obligatòria —mètodes void, espies i reprogramació—, i per què: perquè when(x.metode()) executa el mètode, cosa que en un espia dispara el codi real amb totes les seves conseqüències. Coneixes els valors per defecte d'un mock, inclosos els dos que estalvien més problemes: Optional.empty() i col·leccions buides.
Verifiques interaccions amb verify i tota la seva família —times, never, atLeast, only, inOrder, verifyNoMoreInteractions— sabent que never() és probablement el més valuós, perquè comprovar que alguna cosa no va passar és impossible amb assercions sobre el resultat. I sobretot tens el criteri, que és el que separa les proves bones de les fràgils:
Programa les consultes, verifica les ordres.
Verificar una consulta prova implementació, es trenca en refactoritzar i a més és redundant, perquè l'asserció sobre el resultat ja la comprova indirectament. Verificar una ordre prova un efecte observable que no apareix en cap valor de retorn. Si recordes només una frase d'aquesta lliçó, que sigui aquesta.
Manejes els matchers amb la regla de tots o cap —i saps per què l'InvalidUseOfMatchersException pot aparèixer a la línia o la prova següent, perquè la pila queda contaminada—, i fas servir ArgumentCaptor quan la pregunta no és "es va cridar amb alguna cosa així?" sinó "amb què exactament?", preferint-lo a argThat pels seus missatges d'error.
Coneixes els espies i el seu advertiment: necessitar-ne un sobre la teva pròpia classe gairebé sempre significa que aquesta classe fa dues coses. I coneixes mockStatic amb un advertiment encara més fort: necessitar-lo és gairebé sempre el senyal que el disseny hauria de canviar — si has de simular LocalDate.now(), el que falta és un Clock injectat.
Saps què no s'ha de simular: tipus que no controles —perquè simules la teva creença sobre el seu comportament i la prova pot estar verda mentre el codi falla—, classes de valor, el framework, col·leccions del JDK i la mateixa classe sota prova. I reconeixes el mock excessiu, amb el seu diagnòstic complet: transcriu la implementació, es trenca en refactoritzar, no comprova el resultat, és il·legible i dóna falsa confiança, perquè el mètode pot estar malament i la prova continuar verda.
Distingeixes les proves basades en estat de les basades en interacció, has vist el mateix requisit resolt de les dues formes i saps per què la segona versió és millor: comprova que la multa val realment 5,00 €, calculada pel codi real, en comptes de comprovar que es va passar el valor que el mateix mock va retornar. Amb la recomanació clara: prefereix l'estat; fes servir la interacció només quan l'efecte no sigui observable d'una altra forma.
I tens la lliçó més important de tot el mòdul de proves: de vegades el disseny elimina la necessitat del doble. El Clock injectat de 10-05 en lloc de mockStatic; @ConfigurationProperties en lloc d'un ReglesNegoci estàtic impossible de simular; una funció pura extreta en lloc d'un espia sobre un mateix; un fake reutilitzable en lloc de vint mocks. Abans d'escriure un mock complicat, pregunta't si el problema és que el disseny necessita canviar.
BiblioTech té ara GestorPrestecs provat amb repositoris simulats —inclosos els camins d'error i el reintent davant de conflicte optimista d'11-03—, ServeiAvisos verificat amb ArgumentCaptor fins al contingut del missatge, i el ClientMetadades del mòdul 9 provat sense tocar la xarxa, després d'embolcallar-lo en una interfície pròpia com mana el criteri de no simular el que no controles.
I coneixes el complement necessari: @SpringBootTest amb @MockitoBean —amb el seu avís de rendiment sobre els contextos múltiples—, @DataJpaTest amb H2 i el seu em.clear() sense el qual la prova passaria encara que el mapatge fos incorrecte, Testcontainers com l'estàndard actual per provar contra la base de dades real, i la cobertura amb la seva interpretació honesta: una línia executada no és una línia provada, i un objectiu alt genera proves dolentes.
BiblioTech té ara un projecte Maven reproduïble amb Spring, JPA sobre H2, i una bateria de proves unitàries i d'integració que cobreix el domini, els serveis i la persistència. Es compila amb ./mvnw clean verify i es desplega amb java -jar.
Queden tres deutes del mòdul 10 sense saldar, i els tres són de la mateixa naturalesa: són llibreries, no frameworks.
El JSON de BiblioTech es continua analitzant amb indexOf. Aquell "apedaçament didàctic" de 09-06 porta dos mòduls esperant el seu substitut, i es trenca amb el primer caràcter d'escapada o el primer objecte imbricat. Les entitats continuen tenint cinquanta línies de getters, equals, hashCode i toString escrits a mà — el codi repetitiu que un processador d'anotacions, com el que vas estudiar a 10-02, pot generar sol. I el logging continua sent java.util.logging, triat a 06-07 per no afegir dependències, amb la limitació que ja es va assenyalar allà: l'ecosistema sencer fa servir SLF4J, i mvn dependency:tree t'ha mostrat tres vegades que Spring Boot ja te'l va portar sense que el demanessis.
A la propera lliçó, que tanca el mòdul, se salden els tres. Veuràs Jackson i l'ObjectMapper de veritat —amb les seves anotacions, TypeReference per als genèrics que sobreviuen a l'esborrat de tipus de 10-01, el JavaTimeModule per al java.time de 10-05, i la reescriptura del ClientMetadades que es va prometre fa dos mòduls—; veuràs Lombok, com genera codi en compilació i una valoració honesta dels seus problemes, inclòs @Data en entitats JPA com a font real de bugs; i veuràs SLF4J i Logback, la façana enfront de la implementació, el logging parametritzat amb {} i per què és mesurablement millor que concatenar, i la migració completa de BiblioTech.
I tancaràs el mòdul amb el panorama de les llibreries que tot desenvolupador Java hauria de conèixer — inclosa la que salda l'últim deute pendent: el CSV escrit a mà de 07-07.
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
