La lliçó anterior va acabar amb un límit clar. EstacioService es va poder provar sense Spring perquè existia EstacioRepositoriEnMemoria, un fake escrit a mà al mòdul 2. Però LloguerService —la classe que concentra les regles de negoci de CicloUrbana— depèn de quatre repositoris JPA, del SelectorTarifa, del Clock, de XarxaProperties i del publicador d'esdeveniments d'Spring. Escriure un doble a mà per a cadascun serien centenars de línies de codi que ningú no mantindria, i molts d'aquests tipus són interfícies generades per Spring Data que ni tan sols podem implementar còmodament.
Mockito resol exactament això: genera dobles al vol per a qualsevol interfície o classe, permet programar què retornen i comprovar com se'ls ha cridat. En aquesta lliçó veurem per què calen els dobles, com es creen, com es programen les seves respostes, com funcionen els matchers d'arguments i per què la seva regla de tot-o-res produeix un error desconcertant, com verificar interaccions sense caure en proves fràgils, com capturar els arguments amb ArgumentCaptor, què són els spies i per què gairebé sempre són una olor de disseny, què t'està dient la UnnecessaryStubbingException i quan un fake continua sent millor que cinc línies de stubbing. I ho aplicarem a la prova completa del LloguerService, amb els seus escenaris de bicicleta no disponible, estació plena, durada excedida i càlcul de l'import.
Contingut
- Per què calen dobles
- Tres formes de crear un mock
- Programar respostes: el stubbing
- Matchers d'arguments
- Verificació d'interaccions
ArgumentCaptor- Spies: què són i per què gairebé mai
- Strictness i
UnnecessaryStubbingException - Simulació d'estàtics i constructors
- El cas central:
LloguerServiceTest - Mock o fake: quan cadascun
- Què no s'ha de simular
- Errors Comuns i Consells
- Exercicis
- Per què calen dobles
Tres motius, i tots tres es veuen a CicloUrbana:
Aïllar la unitat. Si LloguerServiceTest fes servir repositoris reals, una fallada podria venir de la consulta, del mapatge, de la transacció o de la lògica. Amb dobles, si la prova falla, l'error és a LloguerService. Aquest és tot el valor d'una prova unitària.
Evitar recursos lents o externs. Base de dades, xarxa, sistema de fitxers, serveis de tercers. Cadascun multiplica el temps per mil i afegeix una raó perquè la prova falli sense que el codi estigui malament.
Forçar escenaris difícils o impossibles. Aquest és el motiu menys evident i el més valuós. Com proves que LloguerService reacciona bé quan el repositori llança una DataAccessException per una fallada de connexió? O quan SelectorTarifa rep un nom de tarifa que no existeix? Provocar-ho de veritat és difícil; amb un doble, és una línia:
Mockito 5 ve inclòs a spring-boot-starter-test. No es declara cap dependència addicional, i fer-ho amb una versió pròpia és la via ràpida a un conflicte al classpath (06-01).
- Tres formes de crear un mock
// 1. Programàtica: útil dins d'un mètode concret
EstacioRepositori repositori = Mockito.mock(EstacioRepositori.class);
// 2. Amb anotacions + extensió: la forma habitual del curs
@ExtendWith(MockitoExtension.class)
class LloguerServiceTest {
@Mock private BicicletaRepositori bicicletaRepositori;
@Mock private LloguerRepositori lloguerRepositori;
@Mock private SelectorTarifa selectorTarifa;
private LloguerService servei; // es construeix a mà al @BeforeEach
@BeforeEach
void prepararServei() {
servei = new LloguerService(bicicletaRepositori, lloguerRepositori,
selectorTarifa, /* ... */);
}
}@ExtendWith(MockitoExtension.class) és el que fa que els camps @Mock s'inicialitzin abans de cada prova —amb el mateix mecanisme de resolució de paràmetres que vam veure a 06-02— i el que activa les comprovacions de strictness de l'apartat 8. Sense l'extensió, els camps anotats queden a null i la prova falla amb un NullPointerException que despista.
La tercera forma és @InjectMocks, i mereix un advertiment:
Sembla còmode, però té tres riscos reals:
| Risc | Què passa |
|---|---|
| Fallada silenciosa | Si falta un @Mock per a una dependència, Mockito injecta null sense avisar; la fallada apareix com a NullPointerException dins del codi de producció |
| Ambigüitat per tipus | Amb dues dependències del mateix tipus, Mockito resol per nom de camp, i un canvi de nom innocent canvia què s'injecta on |
| Amaga el problema de disseny | Si construir la classe a mà resulta incòmode per tenir vuit dependències, això és informació que @InjectMocks amaga |
La política del curs: construir l'objecte amb new al @BeforeEach. És possible precisament perquè a 02-02 vam decidir injectar per constructor: la classe es pot instanciar en dues línies sense cap artefacte màgic. I quan aquestes dues línies comencin a ocupar-ne deu, la prova t'estarà dient que LloguerService té massa responsabilitats.
Per defecte, un mock retorna valors buits: null per a objectes, 0 per a números, false per a booleans, col·leccions buides per a List o Set i Optional.empty() per a Optional. Això últim és molt pràctic: el cas «no trobat» de qualsevol findById no necessita stubbing.
- Programar respostes: el stubbing
// Resposta fixa
when(bicicletaRepositori.findById(1L)).thenReturn(Optional.of(bicicletaRB0142));
// Llançar una excepció
when(lloguerRepositori.save(any())).thenThrow(new DataAccessResourceFailureException("caiguda"));
// Respostes consecutives: la primera crida retorna una cosa, la segona una altra
when(bicicletaRepositori.findById(1L))
.thenReturn(Optional.of(bicicletaRB0142))
.thenReturn(Optional.empty()); // i de la tercera endavant, aquesta
// thenAnswer: la resposta depèn dels arguments rebuts
when(lloguerRepositori.save(any(Lloguer.class))).thenAnswer(invocacio -> {
Lloguer rebut = invocacio.getArgument(0);
rebut.assignarId(99L); // simula el que fa la base de dades
return rebut;
});thenAnswer és l'eina per al cas més comú de tots a Spring Data: save retorna l'entitat amb l'identificador ja assignat. Sense ell, el save simulat retornaria null i el codi de producció fallaria per un motiu que no té res a veure amb el que s'està provant.
Existeix una segona sintaxi, amb el verb al davant:
doReturn(Optional.of(bicicletaRB0142)).when(bicicletaRepositori).findById(1L);
doThrow(new EstacioPlenaException("Plaça Major", 24)).when(estacioService).ancorar(1L);
doNothing().when(publicadorEsdeveniments).publishEvent(any());| Estil | Quan fer-lo servir |
|---|---|
when(...).thenReturn(...) |
Per defecte: és més llegible i comprova tipus en compilació |
doReturn(...).when(...) |
Obligatori amb mètodes void, amb spies i quan el mètode real no s'ha d'executar |
La raó de l'excepció és mecànica: when(mock.metodeVoid()) no compila, perquè void no és una expressió. I amb un spy, when(spy.metode()) executa el mètode real abans de programar-lo, amb els efectes secundaris que això comporti. Amb doReturn la crida no passa mai.
- Matchers d'arguments
Un stub amb un valor literal només respon a aquest valor exacte. Els matchers generalitzen:
| Matcher | Coincideix amb |
|---|---|
any() |
Qualsevol cosa, inclòs null |
any(Lloguer.class), anyLong(), anyString() |
Qualsevol valor no nul del tipus |
eq(1L) |
Aquest valor exacte, en un context que ja usa matchers |
argThat(a -> a.getImportTotal().compareTo(TEN) > 0) |
El que compleixi el predicat |
isNull() / isNotNull() |
Nul o no nul |
anyList(), anyMap(), anySet() |
Col·leccions no nul·les |
La regla de tot-o-res. Si un mètode rep diversos arguments i uses un matcher en un, has de fer servir matchers a tots:
// ❌ Barreja: falla en execució
when(repo.cercarPerEstacioIEstat(1L, any())).thenReturn(List.of());
// ✅ Tots matchers: eq() embolcalla el literal
when(repo.cercarPerEstacioIEstat(eq(1L), any(EstatBicicleta.class))).thenReturn(List.of());L'error que produeix la primera forma és cèlebre pel que costa de llegir:
org.mockito.exceptions.misusing.InvalidUseOfMatchersException: Invalid use of argument matchers! 2 matchers expected, 1 recorded
La causa és a la implementació: els matchers no són valors, sinó que es registren en una pila interna en avaluar-se. Mockito compta els registrats i els compara amb el nombre d'arguments; si no quadren, no pot saber quin era quin. La conseqüència pràctica és que l'error de vegades apareix a la línia següent, en una prova que no hi té res a veure, cosa que fa perdre una bona estona. Si veus un InvalidUseOfMatchersException en un lloc absurd, busca la barreja al stubbing anterior.
Prefereix valors literals quan coneguis l'argument. when(repo.findById(1L)) és més específic i més informatiu que when(repo.findById(anyLong())): si el codi de producció comença a demanar l'identificador 2, vols que la prova t'ho digui, no que continuï passant.
- Verificació d'interaccions
Fins aquí hem fet servir els mocks com a stubs: fonts de dades. També serveixen per comprovar que se'ls ha cridat:
verify(lloguerRepositori).save(any(Lloguer.class)); // exactament una vegada
verify(lloguerRepositori, times(2)).save(any());
verify(lloguerRepositori, never()).delete(any()); // no ha de passar
verify(bicicletaRepositori, atLeastOnce()).findById(1L);
verify(bicicletaRepositori, atMost(3)).findById(anyLong());
verify(selectorTarifa, only()).calcular("estandard", DUES_HORES); // aquesta i cap més
// Ordre entre diversos mocks
InOrder ordre = inOrder(bicicletaRepositori, lloguerRepositori, publicadorEsdeveniments);
ordre.verify(bicicletaRepositori).save(any());
ordre.verify(lloguerRepositori).save(any());
ordre.verify(publicadorEsdeveniments).publishEvent(any(LloguerIniciat.class));
verifyNoMoreInteractions(lloguerRepositori); // res més que el ja verificat
verifyNoInteractions(usuariRepositori); // no s'ha tocat gensI ara l'advertiment, que és el més important de l'apartat. Verificar interaccions acobla la prova a com està escrit el mètode, no a què fa. Una prova plena de verify es trenca amb cada refactorització, encara que el comportament no canviï, i llavors l'equip comença a veure les proves com un impost en lloc d'una xarxa.
| Verifica quan... | No verifiquis quan... |
|---|---|
| La interacció és l'efecte observable: s'ha publicat l'esdeveniment, s'ha enviat el correu, s'ha desat l'entitat | Ja has comprovat el resultat retornat: verificar a més com s'ha calculat és redundant |
Cal comprovar que una cosa no ha passat: never() sobre un esborrat |
El mock és només una font de dades: findById ja s'ha «verificat» en usar-ne la resposta |
| L'ordre importa de veritat per a la correcció | L'ordre és un detall d'implementació |
verifyNoMoreInteractions mereix una menció a part: sembla rigorós i a la pràctica produeix les proves més fràgils de la suite, perquè qualsevol crida afegida —un log, una consulta de suport— la trenca. Reserva'l per als casos en què «no tocar res més» sigui la regla de negoci.
ArgumentCaptor
ArgumentCaptorQuan l'objecte que interessa es passa a un col·laborador en lloc de retornar-se, cal capturar-lo. És exactament el cas de LloguerService.iniciar: construeix un Lloguer internament i el passa a save.
@Test
void desaElLloguerAmbBicicletaEstacioIHoraDInici() {
// Preparar
when(bicicletaRepositori.findById(1L)).thenReturn(Optional.of(bicicletaRB0142));
when(lloguerRepositori.save(any(Lloguer.class))).thenAnswer(i -> i.getArgument(0));
// Actuar
servei.iniciar(new IniciarLloguerRequest(1L, 7L, 1L));
// Comprovar: es captura l'objecte que va rebre el repositori
ArgumentCaptor<Lloguer> capturador = ArgumentCaptor.forClass(Lloguer.class);
verify(lloguerRepositori).save(capturador.capture());
Lloguer desat = capturador.getValue();
assertSoftly(c -> {
c.assertThat(desat.getBicicleta().getMatricula()).isEqualTo("RB-0142");
c.assertThat(desat.getEstacioOrigen().getId()).isEqualTo(1L);
c.assertThat(desat.getInici()).isEqualTo(ARA_A_RIBALTA);
c.assertThat(desat.getFi()).isNull();
c.assertThat(desat.getEstat()).isEqualTo(EstatLloguer.EN_CURS);
});
}Amb @Captor sobre un camp s'evita la línia forClass, i amb getAllValues() es recuperen totes les captures quan el mètode s'ha cridat diverses vegades.
Captor o argThat: argThat(a -> a.getEstat() == EN_CURS) és més curt, però quan falla només diu «no hi ha hagut cap invocació que coincidís», sense mostrar què ha arribat realment. L'ArgumentCaptor produeix un missatge de fallada amb el valor concret. Per a assercions, captor; per seleccionar entre diverses crides, argThat.
- Spies: què són i per què gairebé mai
Un spy embolcalla un objecte real: per defecte tots els seus mètodes s'executen de veritat, i només els que programis se substitueixen.
@Spy private SelectorTarifa selectorTarifa = new SelectorTarifa(List.of(new TarifaEstandard()));
@Test
void faServirLaTarifaRealTretDelCasQueEsSimula() {
doReturn(new BigDecimal("99.00")).when(selectorTarifa).calcular("estandard", DUES_HORES);
// ^ doReturn obligatori: amb when(...) s'executaria el càlcul real primer
}Per què són una olor de disseny. La necessitat de simular part d'una classe significa gairebé sempre que aquesta classe fa dues coses: la que vols provar i la que vols evitar. La solució correcta és extreure la segona a un col·laborador i injectar-lo, que és el que vam fer amb CalculadoraTarifa a 02-02 i amb Clock a 02-01. Un spy és un pedaç sobre un problema estructural.
Els seus dos usos defensables: codi heretat que no pots reestructurar encara, i classes de tercers de les quals només vols alterar un mètode. Fora d'aquí, quan et trobis escrivint @Spy, atura't i mira la classe.
- Strictness i
UnnecessaryStubbingException
UnnecessaryStubbingExceptionMockitoExtension aplica per defecte el mode STRICT_STUBS, que és una de les millors decisions de Mockito 5:
| Mode | Comportament |
|---|---|
LENIENT |
Tot permès; els stubs no usats s'ignoren en silenci |
WARN |
Avisa per consola |
STRICT_STUBS (per defecte) |
Falla si hi ha stubs no usats o crides amb arguments que cap stub no cobreix |
L'error típic:
org.mockito.exceptions.misusing.UnnecessaryStubbingException: Unnecessary stubbings detected. 1. -> at LloguerServiceTest.rebutjaBicicletaEnManteniment(LloguerServiceTest.java:64)
Què t'està dient, que gairebé mai no és «treu aquesta línia». Hi ha tres causes possibles, en ordre de gravetat:
- El codi no arriba on creies. Vas programar
estacioRepositori.findByIdi la validació de la bicicleta talla abans. El stub sobra perquè la prova no està provant el que pensaves: és una troballa, no una molèstia. - La prova arrossega preparació d'una altra. Es va copiar un
@BeforeEachamb cinc stubs i aquest cas només en fa servir dos. Mou al@BeforeEachúnicament el que és comú. - El codi va canviar i la prova no. És el senyal que vols: hi ha stubbing mort.
Si un stub ha d'existir encara que no sempre s'usi, lenient().when(...) l'eximeix, i @MockitoSettings(strictness = Strictness.LENIENT) desactiva el mode a la classe sencera. Fes servir el primer amb moderació i el segon gairebé mai: apagar l'avís perd la informació.
- Simulació d'estàtics i constructors
Des de Mockito 3.4 es poden simular mètodes estàtics, i des de la 3.5 la construcció d'objectes:
try (MockedStatic<LocalDateTime> rellotge = mockStatic(LocalDateTime.class)) {
rellotge.when(LocalDateTime::now).thenReturn(LocalDateTime.of(2026, 3, 14, 10, 0));
// ... dins del try, LocalDateTime.now() retorna aquest valor
} // fora del try, tot torna a la normalitat
try (MockedConstruction<Random> generador = mockConstruction(Random.class,
(simulat, context) -> when(simulat.nextInt(100)).thenReturn(42))) {
// qualsevol new Random() creat aquí dins és un mock
}Dos detalls tècnics importants: el mock estàtic només afecta el fil actual i s'ha de tancar, d'aquí el try amb recursos; si s'escapa, contamina les proves següents amb fallades impossibles d'atribuir.
I ara la part que importa: gairebé sempre és la solució equivocada. Compara les dues formes de provar el càlcul de la durada d'un lloguer:
Amb mockStatic(LocalDateTime.class) |
Amb el Clock injectat (02-01) |
|---|---|
Requereix try amb recursos i sintaxi especial |
Clock.fixed(...) al constructor |
| Només afecta el fil actual: sorpreses amb codi concurrent | Sense efectes globals |
Es trenca si el codi passa a fer servir Instant.now() |
Continua funcionant |
| Instrumentació en temps d'execució, més lenta | Cost zero |
| Amaga que el disseny està acoblat al rellotge del sistema | Fa explícita la dependència |
La conclusió és la de 06-02 dita des de l'altra banda: injecta el que no és determinista en lloc de simular-ho. mockStatic és l'eina per a codi heretat que no pots canviar avui, no una alternativa de disseny.
- El cas central:
LloguerServiceTest
LloguerServiceTestTot l'anterior, aplicat a la classe més important de CicloUrbana.
package com.ciclourbana.lloguers;
@ExtendWith(MockitoExtension.class)
@DisplayName("LloguerService · regles de lloguer de la xarxa de Ribalta")
class LloguerServiceTest {
private static final Instant ARA = Instant.parse("2026-03-14T09:00:00Z");
@Mock private BicicletaRepositori bicicletaRepositori;
@Mock private EstacioRepositori estacioRepositori;
@Mock private UsuariRepositori usuariRepositori;
@Mock private LloguerRepositori lloguerRepositori;
@Mock private SelectorTarifa selectorTarifa;
@Mock private ApplicationEventPublisher publicadorEsdeveniments;
private LloguerService servei;
@BeforeEach
void prepararServei() {
// Construcció explícita: sense @InjectMocks, gràcies a la injecció
// per constructor de 02-02. El rellotge, congelat (06-02).
servei = new LloguerService(bicicletaRepositori, estacioRepositori,
usuariRepositori, lloguerRepositori, selectorTarifa,
publicadorEsdeveniments, Clock.fixed(ARA, ZoneOffset.UTC),
XarxaPropertiesDeProva.perDefecte());
}
@Nested
@DisplayName("en iniciar un lloguer")
class AlIniciar {
@Test
void desaElLloguerIPublicaLEsdevenimentLloguerIniciat() {
when(bicicletaRepositori.findById(1L))
.thenReturn(Optional.of(BicicletesDeProva.rb0142Disponible()));
when(lloguerRepositori.save(any(Lloguer.class)))
.thenAnswer(i -> i.getArgument(0)); // com faria la base de dades
servei.iniciar(new IniciarLloguerRequest(1L, 7L, 1L));
// La publicació de l'esdeveniment SÍ que mereix verify: és un efecte observable
verify(publicadorEsdeveniments).publishEvent(any(LloguerIniciat.class));
}
@Test
void rebutjaLaBicicletaQuanLaBateriaEstaPerSotaDelLlindar() {
when(bicicletaRepositori.findById(1L))
.thenReturn(Optional.of(BicicletesDeProva.ambBateria(12))); // llindar 20
assertThatThrownBy(() -> servei.iniciar(new IniciarLloguerRequest(1L, 7L, 1L)))
.isInstanceOf(BicicletaNoDisponibleException.class)
.hasMessageContaining("RB-0142");
// Res no s'ha d'haver desat ni publicat: aquí el never() és la regla
verify(lloguerRepositori, never()).save(any());
verifyNoInteractions(publicadorEsdeveniments);
}
@Test
void retornaRecursNoTrobatQuanLaBicicletaNoExisteix() {
// Sense stubbing: un mock retorna Optional.empty() per defecte
assertThatThrownBy(() -> servei.iniciar(new IniciarLloguerRequest(99L, 7L, 1L)))
.isInstanceOf(RecursNoTrobatException.class);
}
}
@Nested
@DisplayName("en finalitzar un lloguer")
class AlFinalitzar {
@Test
void calculaLImportAmbLaTarifaDelUsuariIElDesa() {
Lloguer enCurs = LloguersDeProva.enCursDesDe(ARA.minus(Duration.ofMinutes(30)));
when(lloguerRepositori.findById(5L)).thenReturn(Optional.of(enCurs));
when(estacioRepositori.findById(2L))
.thenReturn(Optional.of(EstacionsDeProva.estacioNordAmbEspaiLliure()));
when(selectorTarifa.calcular("estandard", Duration.ofMinutes(30)))
.thenReturn(new BigDecimal("4.10"));
LloguerResponse resposta = servei.finalitzar(5L, new FinalitzarLloguerRequest(2L));
assertThat(resposta.importTotal()).isEqualByComparingTo("4.10");
assertThat(resposta.estat()).isEqualTo(EstatLloguer.FINALITZAT);
assertThat(resposta.duradaMinuts()).isEqualTo(30);
}
@Test
void rebutjaElRetornQuanLEstacioDeDestiEstaPlena() {
when(lloguerRepositori.findById(5L))
.thenReturn(Optional.of(LloguersDeProva.enCursDesDe(ARA)));
when(estacioRepositori.findById(1L))
.thenReturn(Optional.of(EstacionsDeProva.placaMajorPlena()));
assertThatThrownBy(() -> servei.finalitzar(5L, new FinalitzarLloguerRequest(1L)))
.isInstanceOf(EstacioPlenaException.class)
.hasFieldOrPropertyWithValue("codi", "ESTACIO_PLENA");
}
@Test
void aplicaRecarrecQuanSeSuperaLaDuradaMaximaDeDuesHores() {
Lloguer llarg = LloguersDeProva.enCursDesDe(ARA.minus(Duration.ofHours(3)));
when(lloguerRepositori.findById(5L)).thenReturn(Optional.of(llarg));
when(estacioRepositori.findById(2L))
.thenReturn(Optional.of(EstacionsDeProva.estacioNordAmbEspaiLliure()));
when(selectorTarifa.calcular(eq("estandard"), any(Duration.class)))
.thenReturn(new BigDecimal("22.10"));
LloguerResponse resposta = servei.finalitzar(5L, new FinalitzarLloguerRequest(2L));
assertThat(resposta.importTotal()).isEqualByComparingTo("27.10"); // + 5 € de recàrrec
}
}
}Quatre decisions que convé assenyalar, perquè són el resum pràctic de la lliçó:
- El
Clockcongelat al@BeforeEachfa que «trenta minuts» i «tres hores» siguin afirmacions exactes, no aproximacions dependents de quan s'executi la suite. selectorTarifaestà simulat, no és real. La correcció del càlcul de tarifes ja la vam provar a 06-02 amb les proves parametritzades; aquí el que es prova és queLloguerServicedemana la tarifa correcta i fa servir el resultat, inclòs el recàrrec que suma pel seu compte.- Es verifica poc i s'afirma molt. Només hi ha tres
verifya tota la classe, i els tres corresponen a efectes observables reals: publicar l'esdeveniment, no desar res després d'un rebuig i no tocar el publicador. La resta són assercions sobre el valor retornat. - El cas «no trobat» no necessita stubbing, perquè el valor per defecte d'un mock que retorna
OptionalésOptional.empty(). Menys codi i més clar.
- Mock o fake: quan cadascun
CicloUrbana té un fake escrit a mà, EstacioRepositoriEnMemoria, i ara també mocks. No competeixen: serveixen per a coses diferents.
| Mock (Mockito) | Fake (implementació en memòria) | |
|---|---|---|
| Cost inicial | Zero | Escriure i mantenir una classe |
| Preparació per prova | Stubbing explícit a cadascuna | desar(...) i llestos |
| Comportament | Només el programat | Coherent: el que deses, ho llegeixes |
| Verificar interaccions | Sí | No |
| Llegibilitat amb molts casos | Cau en picat | Es manté |
| Risc | Stubs que menteixen sobre el comportament real | El fake es desvia de la implementació real |
La regla pràctica: si una prova necessita més de tres o quatre línies de stubbing per preparar el mateix repositori, o si l'escenari consisteix a «desar alguna cosa i després llegir-la», el fake és millor. Quatre when encadenats per simular una seqüència de lectures són illegibles; un repositori.desar(placaMajor()) no.
I a l'inrevés: si només necessites que un mètode retorni un valor i vols comprovar que s'ha cridat, escriure una classe sencera és un malbaratament.
El risc del fake és real i cal anomenar-lo: pot divergir de la implementació de veritat. EstacioRepositoriEnMemoria filtra amb stream().filter(...), mentre que el repositori JPA genera SQL amb regles diferents d'ordenació i de majúscules. Per això el fake mai no substitueix les proves de 06-04 i 06-05 sobre la base de dades real: és una eina per provar la lògica que el fa servir, no per provar la persistència.
- Què no s'ha de simular
| No simulis | Per què | Què fer |
|---|---|---|
Tipus que no posseeixes (Jackson, JJWT, l'EntityManager) |
El teu mock congela un contracte que el tercer pot canviar; la prova passa i producció falla | Embolcalla'l en una interfície pròpia i simula aquesta; o prova la integració de veritat (06-04) |
Objectes de valor (Estacio, BigDecimal, Duration, record de DTO) |
Són barats de construir i el seu comportament és el que vols provar | new, o una fàbrica EstacionsDeProva |
| La classe sota prova | Un spy parcial sobre ella prova la teva simulació, no el teu codi | Extreu el col·laborador problemàtic |
| Tot, indiscriminadament | Una prova en què tot és mock verifica que Mockito funciona | Fes servir objectes reals quan siguin barats i deterministes |
El primer punt té un exemple dolorós a CicloUrbana: simular JwtParser de JJWT per provar ServeiJwt produiria una prova que passa sempre, perquè el que pot fallar de veritat —la signatura, la caducitat, l'emissor— és just a la part simulada. La prova correcta de ServeiJwt és la de 06-02: JJWT real, Clock injectat.
Errors Comuns i Consells
Oblidar @ExtendWith(MockitoExtension.class). Els camps @Mock queden a null i la fallada apareix dins del codi de producció, lluny de la causa.
Barrejar matchers i literals. InvalidUseOfMatchersException, sovint assenyalant la línia equivocada. Embolcalla els literals amb eq(...).
when(spy.metode()) sobre un spy. Executa el mètode real abans de programar-lo. Amb spies, sempre doReturn(...).when(spy).metode().
Simular mètodes final, static o private sense voler. Mockito 5 fa servir l'inline mock maker per defecte i ja pot amb final, però un mètode private continua sent inabastable: si necessites simular-lo, és que hauria de ser una classe a part.
Sobreverificar. Una prova amb vuit verify i cap asserció sobre el resultat no comprova comportament, comprova una transcripció del mètode. Es trencarà a la primera refactorització.
Silenciar la UnnecessaryStubbingException amb lenient() sense llegir-la. És el consell més important de la lliçó: aquest error és informació, i moltes vegades t'està dient que la prova no executa el camí que creus.
Stubs que menteixen. when(repo.save(any())).thenReturn(lloguerAmbImport) fa que la prova passi encara que el servei no calculi mai l'import: el valor l'hi vas posar tu. Retorna sempre el que retornaria el col·laborador real —amb thenAnswer(i -> i.getArgument(0)) per als save— i afirma sobre el que produeix el codi.
Consell: prepara al @BeforeEach només el que fan servir totes les proves. La resta va a cada prova, on es llegeix al costat del que verifica. Amb STRICT_STUBS, a més, el contrari falla.
Consell: anomena les dades, no els mocks. BicicletesDeProva.ambBateria(12) diu per què aquest valor importa; bicicleta1 no diu res.
Exercicis
Exercici 1
Escriu una prova de LloguerService.iniciar que verifiqui, fent servir ArgumentCaptor, que l'esdeveniment LloguerIniciat publicat porta l'identificador del lloguer acabat de desar i l'instant del Clock injectat, i no Instant.now() del sistema. Explica per què aquesta prova fallaria si algú substituís el Clock per una crida estàtica, i per què això és exactament el que es vol.
Exercici 2
SeguretatLloguers.esPropietari(idLloguer, usuari) de 05-05 consulta lloguerRepositori.existsByIdAndUsuariId. Escriu la seva classe de prova amb Mockito cobrint quatre casos: és el propietari, no ho és, idLloguer nul i usuari nul. Para atenció a un detall: en dos dels quatre casos el repositori no s'ha de consultar gens. Verifica-ho i raona per què aquest verify sí que està justificat.
Exercici 3
Aquesta prova té cinc defectes dels vistos a la lliçó. Enumera'ls i reescriu-la:
@ExtendWith(MockitoExtension.class)
class LloguerServiceTest {
@Mock LloguerRepositori lloguerRepositori;
@Mock BicicletaRepositori bicicletaRepositori;
@Mock SelectorTarifa selectorTarifa;
@InjectMocks LloguerService servei;
@Test
void test() {
when(bicicletaRepositori.findById(anyLong())).thenReturn(Optional.of(new Bicicleta()));
when(selectorTarifa.calcular(anyString(), any())).thenReturn(new BigDecimal("4.1"));
when(lloguerRepositori.save(any())).thenReturn(new Lloguer());
servei.iniciar(new IniciarLloguerRequest(1L, 7L, 1L));
verify(bicicletaRepositori).findById(anyLong());
verify(lloguerRepositori).save(any());
verify(lloguerRepositori, times(1)).save(any());
}
}Solucions
Solució 1
@Test
void publicaLEsdevenimentAmbIdDelLloguerIHoraDelRellotgeInjectat() {
when(bicicletaRepositori.findById(1L))
.thenReturn(Optional.of(BicicletesDeProva.rb0142Disponible()));
when(lloguerRepositori.save(any(Lloguer.class))).thenAnswer(invocacio -> {
Lloguer rebut = invocacio.getArgument(0);
rebut.assignarId(99L); // el que fa la seqüència de PostgreSQL
return rebut;
});
ArgumentCaptor<LloguerIniciat> capturador =
ArgumentCaptor.forClass(LloguerIniciat.class);
servei.iniciar(new IniciarLloguerRequest(1L, 7L, 1L));
verify(publicadorEsdeveniments).publishEvent(capturador.capture());
assertSoftly(c -> {
c.assertThat(capturador.getValue().idLloguer()).isEqualTo(99L);
c.assertThat(capturador.getValue().moment()).isEqualTo(ARA);
});
}Comentari: el thenAnswer que assigna l'identificador és imprescindible. Sense ell, save retornaria null o un lloguer sense id, i l'asserció sobre 99L no podria distingir «el servei no propaga l'id» de «el mock no l'hi va posar». Aquí es veu també per què el captor és superior a argThat en aquest cas: si el moment no coincidís, el missatge mostra l'instant rebut, i amb argThat només diria que no hi ha hagut coincidències.
Per què fallaria amb Instant.now(): l'asserció compara contra ARA, l'instant exacte del Clock.fixed del @BeforeEach. Una crida estàtica retornaria l'hora real de l'execució, que mai no és aquesta. I això és just el que es busca: la prova actua com a guardiana del disseny. Si demà algú «simplifica» el codi traient el Clock, la suite es posa en vermell assenyalant la regressió de disseny abans que arribi a producció. És la quarta raó per provar de 06-01 —la pressió de disseny— convertida en una asserció concreta.
Solució 2
@ExtendWith(MockitoExtension.class)
@DisplayName("SeguretatLloguers · @PreAuthorize de 05-05")
class SeguretatLloguersTest {
@Mock private LloguerRepositori lloguerRepositori;
private SeguretatLloguers seguretat;
@BeforeEach
void prepararComponent() {
seguretat = new SeguretatLloguers(lloguerRepositori);
}
@Test
void permetAlCiutadaGestionarElSeuPropiLloguer() {
when(lloguerRepositori.existsByIdAndUsuariId(9L, 7L)).thenReturn(true);
assertThat(seguretat.esPropietari(9L, UsuarisDeProva.marta())).isTrue();
}
@Test
void denegaAlCiutadaElLloguerDunAltre() {
when(lloguerRepositori.existsByIdAndUsuariId(9L, 7L)).thenReturn(false);
assertThat(seguretat.esPropietari(9L, UsuarisDeProva.marta())).isFalse();
}
@Test
void denegaSenseConsultarLaBaseDeDadesQuanLIdentificadorEsNul() {
assertThat(seguretat.esPropietari(null, UsuarisDeProva.marta())).isFalse();
verifyNoInteractions(lloguerRepositori);
}
@Test
void denegaSenseConsultarLaBaseDeDadesQuanNoHiHaUsuariAutenticat() {
assertThat(seguretat.esPropietari(9L, null)).isFalse();
verifyNoInteractions(lloguerRepositori);
}
}Per què el verifyNoInteractions està justificat aquí, quan l'apartat 5 advertia contra la sobreverificació: perquè en aquests dos casos no consultar és part del comportament correcte, no un detall d'implementació. Si el codi fes existsByIdAndUsuariId(null, null), el resultat continuaria sent false i una asserció sobre el valor retornat no distingiria totes dues versions; però la segona llança una consulta a PostgreSQL a cada petició anònima, i en un endpoint públic això és una via de saturació. La regla: verifica una absència d'interacció quan aquesta absència és la garantia que vols.
Cal notar a més que aquestes quatre proves s'executen en mil·lisegons i ja converteixen en assercions la meitat de la regla de seguretat de 05-05. L'altra meitat —que @PreAuthorize estigui posada i Spring l'apliqui— no es pot comprovar aquí: necessita el context, i és la feina de 06-04.
Solució 3
Els cinc defectes:
@InjectMocks: siLloguerServiceté més dependències de les declarades —i en té:EstacioRepositori,Clock, el publicador—, Mockito injectanullen silenci.test()com a nom: no descriu cap regla; ni l'informe ni la fallada no diran res.anyLong()ianyString()on es coneixen els valors: si el codi comença a demanar un altre identificador o la tarifa «estudiant», la prova continua passant. Els matchers laxos amaguen regressions.when(selectorTarifa.calcular(...))no es fa servir ainiciar: la tarifa només es calcula en finalitzar. AmbSTRICT_STUBSaixò és unaUnnecessaryStubbingException, i el que assenyala és que la prova barreja dos fluxos.- Sobreverificació i cap asserció: tres
verify(dos d'ells equivalents, perquèverify(x)ja significatimes(1)) i zeroassertThatsobre el resultat. És la prova sense assercions de 06-01 disfressada. S'hi suma un sisè problema de fons:when(save(any())).thenReturn(new Lloguer())és un stub que menteix, perquè retorna un lloguer buit que no té res a veure amb el que se li va passar.
Reescrita, amb un sol comportament per prova:
@Test
void desaElLloguerAmbLaBicicletaSolicitadaIPublicaLEsdeveniment() {
when(bicicletaRepositori.findById(1L))
.thenReturn(Optional.of(BicicletesDeProva.rb0142Disponible()));
when(lloguerRepositori.save(any(Lloguer.class))).thenAnswer(i -> i.getArgument(0));
LloguerResponse resposta = servei.iniciar(new IniciarLloguerRequest(1L, 7L, 1L));
assertThat(resposta.matriculaBicicleta()).isEqualTo("RB-0142");
assertThat(resposta.estat()).isEqualTo(EstatLloguer.EN_CURS);
verify(publicadorEsdeveniments).publishEvent(any(LloguerIniciat.class));
}El càlcul de la tarifa es trasllada a la seva pròpia prova dins del bloc AlFinalitzar, que és on passa.
Conclusió
LloguerService ha deixat de ser la part no provada de CicloUrbana. Saps per què calen els dobles —aïllar la unitat, evitar recursos lents i, sobretot, forçar escenaris que seria difícil provocar de veritat— i les tres formes de crear-los, amb la política del curs ben fonamentada: @Mock amb MockitoExtension i construcció explícita de l'objecte al @BeforeEach, sense @InjectMocks, perquè la injecció per constructor de 02-02 ho fa innecessari i perquè el dia que construir la classe resulti incòmode, aquesta incomoditat és informació que no convé amagar.
Domines el stubbing en les seves dues sintaxis i saps quan doReturn deixa de ser una alternativa estilística i passa a ser obligatori: mètodes void i spies. Coneixes els matchers, la regla de tot-o-res i el motiu mecànic —la pila interna— pel qual InvalidUseOfMatchersException apareix de vegades a la línia equivocada. Saps verificar amb times, never, inOrder i verifyNoInteractions, i saps el més difícil: verificar poc, reservant-ho per als efectes observables reals, perquè una prova plena de verify és una transcripció del mètode que es trenca a la primera refactorització. Captures arguments amb ArgumentCaptor quan l'objecte interessant es passa a un col·laborador —el cas del Lloguer que va a save i de l'esdeveniment LloguerIniciat—, i saps quan el captor guanya a argThat: quan el missatge de fallada importa.
Has vist per què els spies són gairebé sempre una olor de disseny i per què mockStatic no ha de competir amb injectar un Clock, i has après a llegir la UnnecessaryStubbingException com el que és: tres diagnòstics possibles, el més interessant dels quals és «la teva prova no està executant el camí que creus». Tens la comparació entre mock i fake amb un criteri operatiu —més de tres o quatre línies de stubbing sobre el mateix repositori, o un escenari de desar-i-llegir, demanen un fake— i la llista del que no se simula mai: els tipus que no posseeixes, els objectes de valor i la classe sota prova.
Però mira el que continua sense estar cobert. Que @PreAuthorize estigui posada sobre LloguerService.finalitzar i que Spring l'apliqui de veritat; que EstacioController retorni un 404 amb format ProblemDetail; que findByUsuariIdAndFiIsNull generi el SQL correcte; que les regles d'authorizeHttpRequests tanquin el que creiem. Res d'això no es pot comprovar amb mocks, perquè el que falla allà no és la lògica: és el cablejat, les anotacions i el framework. La lliçó següent, Proves d'Integració, aixeca el context d'Spring amb criteri: @SpringBootTest i la seva memòria cau de contextos, les llesques @WebMvcTest i @DataJpaTest, MockMvc, @MockitoBean —el substitut de l'obsolet @MockBean— i les anotacions d'spring-security-test que convertiran en assercions automàtiques cada regla del mòdul 5.
Curs de Spring Boot
Mòdul 1: Introducció a Spring Boot
- Què és Spring Boot?
- Configuració del teu entorn de desenvolupament
- Creant la teva primera aplicació Spring Boot
- Entenent l'estructura del projecte
- L'arrencada i el cicle de vida de l'aplicació
Mòdul 2: Conceptes bàsics de Spring Boot
- Anotacions de Spring Boot
- Injecció de dependències a Spring Boot
- Àmbit i cicle de vida dels beans
- Configuració de Spring Boot
- Propietats de Spring Boot
- Autoconfiguració i starters per dins
Mòdul 3: Construint serveis web RESTful
- Introducció als serveis web RESTful
- Creant controladors REST
- Gestió dels mètodes HTTP
- Validació de dades d'entrada
- DTOs i mapatge entre capes
- Gestió d'excepcions a REST
- Documentar l'API amb OpenAPI
Mòdul 4: Accés a dades amb Spring Boot
- Introducció a Spring Data JPA
- Configuració de fonts de dades
- Creació d'entitats JPA
- Relacions entre entitats
- Ús de repositoris de Spring Data
- Mètodes de consulta a Spring Data JPA
- Transaccions i gestió de la persistència
- Migracions d'esquema amb Flyway
Mòdul 5: Seguretat a Spring Boot
- Introducció a Spring Security
- Configuració de Spring Security
- Autenticació i autorització d'usuaris
- Implementació d'autenticació JWT
- Seguretat a nivell de mètode i enduriment de l'API
Mòdul 6: Proves a Spring Boot
- Introducció a les proves
- Proves unitàries amb JUnit
- Simulació amb Mockito
- Proves d'integració
- Proves amb Testcontainers
Mòdul 7: Funcions avançades de Spring Boot
- Spring Boot Actuator
- Perfils de Spring Boot
- Tasques programades i execució asíncrona
- Spring Boot amb Docker
- Spring Boot i microserveis
- Comunicació entre serveis i tolerància a fallades
Mòdul 8: Desplegament d'aplicacions Spring Boot
- Introducció al desplegament
- Desplegant a Heroku
- Desplegant a AWS
- Desplegant a Kubernetes
- Integració i lliurament continus
Mòdul 9: Rendiment i monitoratge
- Ajust de rendiment
- Memòria cau amb Spring Cache
- Monitoratge amb Spring Boot Actuator
- Ús de Prometheus i Grafana
- Gestió de registres i logs
- Traçabilitat distribuïda
