La lliçó anterior va deixar LloguerService ben provat i una llista incòmoda de coses que els mocks no poden abastar. Que @PreAuthorize estigui realment posada sobre finalitzar i que Spring l'apliqui. Que EstacioController tradueixi una RecursNoTrobatException en un 404 amb cos ProblemDetail. Que findByUsuariIdAndFiIsNull generi el SQL que creiem. Que anyRequest().denyAll() tanqui el que ha de tancar. Tot això té una cosa en comú: el que pot fallar no és la lògica, sinó el cablejat, les anotacions i el comportament del framework, i un mock no en sap res.
Aquesta lliçó aixeca el context d'Spring, però amb criteri. Veurem què arrenca @SpringBootTest i els seus quatre entorns web, la memòria cau de contextos que decideix si la teva suite triga quaranta segons o set minuts, les proves de llesca que carreguen només una capa, MockMvc per provar controladors, @DataJpaTest per a consultes, TestRestTemplate i WebTestClient per al procés complet, i les anotacions d'spring-security-test que convertiran en assercions automàtiques cada regla del mòdul 5. Al final, la promesa que obria el mòdul estarà complerta: un ciutadà que intenti finalitzar el lloguer d'un altre produirà un 403 comprovat per una prova, no per bona voluntat.
Contingut
- Què afegeix una prova d'integració
@SpringBootTestiwebEnvironment- La memòria cau del context d'aplicació
- Les proves de llesca
@WebMvcTestambMockMvc- Provar errors i validació
MockMvcTester, l'API fluida@DataJpaTestTestRestTemplateiWebTestClient- Provar la seguretat del mòdul 5
- Configuració específica de proves
- Dades amb
@SqliApplicationContextRunner - Errors Comuns i Consells
- Exercicis
- Què afegeix una prova d'integració
Una prova d'integració a Spring Boot arrenca un context d'aplicació i prova diverses peces treballant juntes. El que aporta sobre les unitàries és exactament el que les unitàries no poden veure:
| Fallada real possible a CicloUrbana | La veu una unitària? |
|---|---|
Falta @Valid al @RequestBody, i la validació de 03-04 no s'aplica |
No |
El GestorGlobalExcepcions no captura EstacioPlenaException |
No |
| Una consulta derivada mal anomenada que Spring Data no sap traduir | No: falla en crear el context |
@Transactional sobre un mètode private, que el proxy no intercepta |
No |
Una regla d'authorizeHttpRequests en l'ordre equivocat |
No |
Un Instant que Jackson serialitza com a número en lloc d'ISO-8601 |
No |
| El càlcul del recàrrec per excés de durada | Sí, i aquí s'ha de provar |
L'última fila és el contrast important: el que una unitària pot provar, es prova en una unitària. La integració és cara —segons davant de mil·lisegons— i es reserva per al que només ella detecta.
@SpringBootTest i webEnvironment
@SpringBootTest i webEnvironment@SpringBootTest busca cap amunt als paquets la classe anotada amb @SpringBootApplication —per això la prova ha d'estar a com.ciclourbana o per sota— i arrenca el context complet: tots els beans, l'autoconfiguració de 02-06, la font de dades, Flyway i la cadena de filtres de seguretat.
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class LloguerFluxCompletIT { /* ... */ }webEnvironment |
Què aixeca | Com es crida l'API | Quan usar-lo |
|---|---|---|---|
MOCK (per defecte) |
Context web simulat, sense servidor | MockMvc |
Gairebé sempre: ràpid i suficient |
RANDOM_PORT |
Tomcat real en un port lliure | TestRestTemplate, WebTestClient |
Provar serialització, filtres i codis HTTP reals |
DEFINED_PORT |
Tomcat al port d'application.yml |
Igual | Gairebé mai: col·lisiona en integració contínua |
NONE |
Context sense capa web | Cridant els beans | Provar serveis i repositoris amb el context complet |
RANDOM_PORT evita el clàssic «Address already in use» quan dues construccions corren alhora al mateix agent. El port assignat s'injecta amb @LocalServerPort int port, encara que TestRestTemplate ja el resol sol amb rutes relatives.
El cost que cal tenir present: arrencar el context complet de CicloUrbana —JPA, Hibernate, HikariCP, Flyway, Spring Security, springdoc— triga entre dos i sis segons. Cent proves així serien deu minuts, i una suite de deu minuts deixa d'executar-se. La solució té dues parts: la memòria cau de l'apartat següent i les llesques de l'apartat 4.
- La memòria cau del context d'aplicació
Aquesta és la clau del rendiment de tota la suite, i qui no l'entén acaba amb una suite lenta sense saber per què.
Spring Test no crea un context per classe de prova: manté una memòria cau i reutilitza el context entre classes sempre que la configuració sigui idèntica. La clau de memòria cau es compon de les classes de configuració, els perfils actius, les propietats, els inicialitzadors i altres elements. Si dues classes demanen la mateixa configuració, el context s'arrenca una vegada i totes dues van ràpides.
flowchart LR
A["EstacioServiceIT<br/>@SpringBootTest"] --> K["Clau: config + perfils<br/>+ propietats + mocks"]
B["LloguerServiceIT<br/>@SpringBootTest"] --> K
C["SeguretatIT<br/>@SpringBootTest + @MockitoBean"] --> K2["Clau DIFERENT"]
K --> CTX1["Context 1 · s'arrenca una vegada"]
K2 --> CTX2["Context 2 · una altra arrencada de 4 s"]
Què invalida la memòria cau i obliga a un context nou:
| Element | Efecte |
|---|---|
@MockitoBean / @MockitoSpyBean |
Context nou per cada combinació diferent de beans substituïts |
@TestPropertySource(properties = ...) |
Context nou per cada joc de propietats |
@ActiveProfiles("altre") |
Context nou per cada combinació de perfils |
@DirtiesContext |
Destrueix el context després de la classe o el mètode |
Llesca diferent (@WebMvcTest davant de @DataJpaTest) |
Contextos diferents per definició |
Consells concrets per no fragmentar-la:
- Estandarditza. Que totes les proves d'integració facin servir
@ActiveProfiles("test")i les mateixes propietats. Una classe amb un perfil diferent «només per a aquesta prova» costa quatre segons a la suite. - Centralitza en una classe base. Una
ProvaIntegracioBaseanotada una sola vegada, de la qual hereten totes, garanteix una única clau de memòria cau. És el patró que 06-05 estendrà amb el contenidor de PostgreSQL. - Agrupa els
@MockitoBean. Si tres classes simulen el mateix bean, que el declarin igual: comparteixen context. Si cadascuna en simula un de diferent, són tres contextos. @DirtiesContextés l'últim recurs. Es fa servir quan una prova modifica el context de forma irreversible. Cada ús afegeix una arrencada completa; si apareix a diverses classes, gairebé sempre el problema real és una prova que no neteja el que embruta.
Per veure què està passant, activa el registre del gestor de memòria cau:
I veuràs línies com Spring test ApplicationContext cache statistics: [size = 4, hitCount = 37, missCount = 4]. Un missCount alt és el símptoma d'una suite fragmentada.
- Les proves de llesca
Una llesca (slice) carrega només la part del context que necessita una capa: els beans d'aquesta capa i la seva autoconfiguració, i res més. Són molt més ràpides que @SpringBootTest i la seva fallada és molt més precisa.
| Anotació | Què autoconfigura | Què no carrega | Per provar |
|---|---|---|---|
@WebMvcTest |
DispatcherServlet, controladors, @ControllerAdvice, Jackson, MockMvc, filtres de seguretat |
@Service, @Repository, JPA, font de dades |
EstacioController, LloguerController |
@DataJpaTest |
JPA, Hibernate, repositoris, TestEntityManager, base de dades incrustada, transacció amb rollback |
Controladors, serveis, seguretat web | LloguerRepositori, entitats, consultes |
@JsonTest |
Jackson i els seus ObjectMapper, JacksonTester |
Tota la resta | Serialització de DTOs de 03-05 |
@RestClientTest |
RestTemplateBuilder/RestClient i MockRestServiceServer |
Tota la resta | El client del servei extern de 07-06 |
@JdbcTest / @DataJdbcTest |
JdbcTemplate / Spring Data JDBC, sense JPA |
JPA, controladors | Projectes sense JPA |
La conseqüència pràctica de la columna «què no carrega»: en un @WebMvcTest, l'EstacioService que el controlador necessita no existeix, i el context falla en arrencar si no el proporciones. Aquí entra @MockitoBean.
@WebMvcTest amb MockMvc
@WebMvcTest amb MockMvcpackage com.ciclourbana.estacions;
@WebMvcTest(EstacioController.class) // només AQUEST controlador; sense això, tots
@DisplayName("EstacioController · API pública d'estacions")
class EstacioControllerTest {
@Autowired private MockMvc mockMvc;
@Autowired private ObjectMapper objectMapper;
/** Substitueix el bean real del context per un mock de Mockito.
* Des d'Spring Boot 3.4, @MockBean està OBSOLETA en favor d'aquesta. */
@MockitoBean private EstacioService estacioService;
@Test
void retornaLEstacioIElsSeusCampsQuanExisteix() throws Exception {
when(estacioService.obtenirPerId(1L))
.thenReturn(new EstacioResponse(1L, "Plaça Major", "Plaça Major, 1", 24, 9));
mockMvc.perform(get("/api/v1/estacions/{id}", 1L))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.nom").value("Plaça Major"))
.andExpect(jsonPath("$.capacitat").value(24))
.andExpect(jsonPath("$.contrasenyaHash").doesNotExist()); // no filtrar
}
@Test
void creaLEstacioIRetornaLaCapcaleraLocation() throws Exception {
CrearEstacioRequest peticio =
new CrearEstacioRequest("Mercat Vell", "Carrer del Mercat, 8", 18);
when(estacioService.crear(any())).thenReturn(
new EstacioResponse(5L, "Mercat Vell", "Carrer del Mercat, 8", 18, 18));
mockMvc.perform(post("/api/v1/estacions")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(peticio)))
.andExpect(status().isCreated())
.andExpect(header().string("Location", "/api/v1/estacions/5"));
}
}Anatomia de la crida, peça a peça:
@WebMvcTest(EstacioController.class)limita la llesca a un controlador. Sense l'argument carrega tots, cosa que arrenca més lent i fa que una fallada en un altre controlador trenqui aquesta prova.@MockitoBean(paquetorg.springframework.test.context.bean.override.mockito) substitueix el bean al context.@MockBeanestà obsoleta des d'Spring Boot 3.4 i desapareixerà; la nova anotació pertany al mecanisme genèric de bean overriding d'Spring Framework 6.2, del qual també formen part@MockitoSpyBeani@TestBean. Recorda de l'apartat 3 que cada combinació diferent de@MockitoBeancrea un context nou.get(...),post(...),put(...),delete(...)són estàtics deMockMvcRequestBuilders. Accepta variables de ruta com a arguments ("/{id}", 1L), i admet.param(...),.header(...),.contentType(...),.accept(...).status(),jsonPath(),header(),content()són estàtics deMockMvcResultMatchers.jsonPathfa servir Hamcrest, que és l'única excepció a la regla «només AssertJ» de 06-02..andDo(print())aboca petició i resposta a la consola: és el primer que s'afegeix quan alguna cosa no quadra.
L'asserció jsonPath("$.contrasenyaHash").doesNotExist() és més important del que sembla: és la prova de contracte que 03-05 prometia, la que detecta que algú ha afegit un camp intern a la resposta pública.
Què prova i què no una llesca web. Prova el mapatge de rutes, la deserialització del cos, la validació, la serialització de la resposta, els codis d'estat, les capçaleres i els gestors d'excepcions. No prova la lògica del servei —està simulada— ni la persistència. És exactament la divisió correcta.
- Provar errors i validació
Els ProblemDetail de 03-06 i les respostes de validació de 03-04 són contracte públic, així que mereixen proves pròpies:
@Test
void retorna404AmbFormatProblemDetailQuanEstacioNoExisteix() throws Exception {
when(estacioService.obtenirPerId(99L))
.thenThrow(new RecursNoTrobatException("Estació", 99L));
mockMvc.perform(get("/api/v1/estacions/99"))
.andExpect(status().isNotFound())
.andExpect(content().contentType(MediaType.APPLICATION_PROBLEM_JSON))
.andExpect(jsonPath("$.type").value("https://api.ciclourbana.example/errors/recurs-no-trobat"))
.andExpect(jsonPath("$.title").value("Recurs no trobat"))
.andExpect(jsonPath("$.status").value(404))
.andExpect(jsonPath("$.detail").value(containsString("99")))
.andExpect(jsonPath("$.instance").value("/api/v1/estacions/99"));
}
@Test
void retorna400AmbLaLlistaDeCampsInvalidsEnCrearUnaEstacioMalFormada() throws Exception {
String cos = """
{"nom": "", "adreca": "Carrer Fals, 1", "capacitat": -5}
""";
mockMvc.perform(post("/api/v1/estacions")
.contentType(MediaType.APPLICATION_JSON).content(cos))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errors", hasSize(2)))
.andExpect(jsonPath("$.errors[*].camp",
containsInAnyOrder("nom", "capacitat")));
verifyNoInteractions(estacioService); // la petició no va arribar al servei
}Aquest últim verifyNoInteractions és l'asserció que fa valuosa la segona prova: comprova que la validació talla a la vora, que és just el que 03-04 pretenia. I la resposta d'error es prova aquí, i no en una unitària, perquè la produeix la cooperació entre @Valid, MethodArgumentNotValidException i el @RestControllerAdvice: cap de les tres peces per separat no la genera.
Per comparar el JSON complet en lloc de camp a camp, content().json(esperat) fa servir JSONassert i ignora l'ordre de les claus; amb content().json(esperat, true) la comparació és estricta i falla si hi sobra algun camp.
MockMvcTester, l'API fluida
MockMvcTester, l'API fluidaSpring Framework 6.2 introdueix MockMvcTester, una capa sobre MockMvc amb assercions d'AssertJ i sense throws Exception:
@Autowired private MockMvcTester mockMvc; // s'autoconfigura a @WebMvcTest
@Test
void retornaLEstacioAmbLApiFluida() {
assertThat(mockMvc.get().uri("/api/v1/estacions/{id}", 1L))
.hasStatusOk()
.hasContentType(MediaType.APPLICATION_JSON)
.bodyJson().extractingPath("$.nom").isEqualTo("Plaça Major");
}MockMvc clàssic |
MockMvcTester |
|
|---|---|---|
| Estil | andExpect(...) encadenats |
assertThat(...) d'AssertJ |
| Excepcions | throws Exception a cada mètode |
No |
| Matchers | Hamcrest | AssertJ |
| Maduresa | Ubic: tota la documentació i els exemples | Recent |
És més coherent amb la resta del curs, però aquest mòdul fa servir MockMvc clàssic per una raó pràctica: és el que trobaràs en qualsevol projecte i en qualsevol resposta que busquis. Convé conèixer totes dues i no barrejar-les dins d'una mateixa classe.
@DataJpaTest
@DataJpaTest@DataJpaTest
@DisplayName("LloguerRepositori · consultes de la xarxa de Ribalta")
class LloguerRepositoriTest {
@Autowired private LloguerRepositori lloguerRepositori;
@Autowired private TestEntityManager em;
@Test
void findByUsuariIdAndFiIsNullRetornaNomesElsLloguersEnCurs() {
Usuari marta = em.persist(UsuarisDeProva.novaMarta());
em.persist(LloguersDeProva.enCursDe(marta));
em.persist(LloguersDeProva.finalitzatDe(marta));
em.flush(); // força el SQL: sense això, la consulta pot no veure'ls
em.clear(); // buida la memòria cau de primer nivell: lectures reals
List<Lloguer> enCurs = lloguerRepositori.findByUsuariIdAndFiIsNull(marta.getId());
assertThat(enCurs).hasSize(1)
.allSatisfy(a -> assertThat(a.getFi()).isNull());
}
}Tres característiques que cal entendre:
Base de dades incrustada per defecte. Si H2 és al classpath, @DataJpaTest substitueix la font de dades real per una en memòria. És ràpid i és la raó de ser de la lliçó 06-05: H2 no és PostgreSQL, i hi ha consultes que aquí passen i a Ribalta fallen. Per fer servir la font de dades configurada s'afegeix @AutoConfigureTestDatabase(replace = Replace.NONE).
Transacció amb rollback automàtic. Cada prova corre en una transacció que es desfà en acabar, així que les proves no es contaminen entre si i no cal netejar res. Les seves dues implicacions:
- Tot passa a la mateixa sessió d'Hibernate, així que una entitat persistida continua a la memòria cau de primer nivell i una consulta podria retornar-la sense tocar la base de dades. D'aquí l'
em.flush()i l'em.clear(): sense ells la prova pot passar encara que el SQL generat sigui incorrecte. És l'error més comú de@DataJpaTest. - El rollback amaga les fallades de restriccions diferides i no prova el comportament del
commitreal. Si això importa,@Transactional(propagation = NOT_SUPPORTED)sobre la prova, netejant a mà.
TestEntityManager és un embolcall sobre l'EntityManager amb mètodes pensats per a proves: persist, persistFlushFind, flush, clear, refresh. Es fa servir per preparar dades; el que es verifica és sempre el repositori, que és el que s'està provant.
TestRestTemplate i WebTestClient
TestRestTemplate i WebTestClientAmb RANDOM_PORT hi ha un Tomcat real i es crida l'API per HTTP de veritat, dins del mateix procés:
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
class EstacioApiIT {
@Autowired private TestRestTemplate client; // rutes relatives: resol el port
@Test
void llistaLesQuatreEstacionsDeRibaltaPaginades() {
ResponseEntity<String> resposta =
client.getForEntity("/api/v1/estacions?size=10", String.class);
assertThat(resposta.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(resposta.getBody()).contains("Plaça Major", "Universitat");
}
}WebTestClient és l'alternativa moderna, fluida i vàlida també per a WebFlux; requereix spring-webflux al classpath de prova i s'autoconfigura amb @AutoConfigureWebTestClient.
MockMvc |
TestRestTemplate / WebTestClient |
|
|---|---|---|
| Servidor | No: simula el DispatcherServlet |
Tomcat real |
| Serialització | Real (Jackson) | Real |
| Capa de xarxa i filtres de Servlet | No els travessa | Sí, tots |
| Velocitat | Mil·lisegons | Desenes de mil·lisegons + arrencada |
| Accés als beans interns | Sí, el context és a mà | Sí |
| Ús recomanat | El 90 % de les proves de la capa web | Uns pocs camins crítics complets |
TestRestTemplate té una diferència útil davant de RestTemplate: no llança excepció amb els codis 4xx i 5xx, cosa que permet afirmar sobre un 404 sense embolcallar la crida en un try.
- Provar la seguretat del mòdul 5
Aquí es compleix la promesa del tancament del mòdul 5. Primer, la dependència:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>| Anotació / utilitat | Què fa |
|---|---|
@WithMockUser(roles = "OPERARI") |
Autenticació simulada, sense tocar la base de dades |
@WithAnonymousUser |
Petició explícitament anònima |
@WithUserDetails("[email protected]") |
Carrega l'usuari real amb ServeiDetallsUsuari: necessita dades |
@WithSecurityContext |
Base per a anotacions pròpies |
SecurityMockMvcRequestPostProcessors.user(...) |
El mateix, però per petició: .with(user(...)) |
...jwt() / ...csrf() |
Simular un token de recurs OAuth2 o afegir el testimoni CSRF |
Un detall que cal fixar: @WithMockUser(roles = "ADMIN") afegeix l'autoritat ROLE_ADMIN, perquè li posa el prefix. Amb authorities = "ROLE_ADMIN" s'indica l'autoritat literal. Confondre'ls —escriure roles = "ROLE_ADMIN" i acabar amb ROLE_ROLE_ADMIN— és un clàssic que produeix un 403 inexplicable.
En una llesca web cal importar la configuració de seguretat, perquè @WebMvcTest carrega els filtres però no la teva ConfiguracioSeguretat:
@WebMvcTest(LloguerController.class)
@Import({ConfiguracioSeguretat.class, ServeiJwt.class})
class LloguerControllerSeguretatTest {
@Autowired private MockMvc mockMvc;
@MockitoBean private LloguerService lloguerService;
@MockitoBean private ServeiDetallsUsuari serveiDetallsUsuari;
@Test
@WithAnonymousUser
void exigeixAutenticacioPerConsultarElsLloguers() throws Exception {
mockMvc.perform(get("/api/v1/lloguers")).andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(roles = "CIUTADA")
void permetAlCiutadaConsultarElsSeusLloguers() throws Exception {
mockMvc.perform(get("/api/v1/lloguers")).andExpect(status().isOk());
}
@Test
@WithMockUser(roles = "CIUTADA")
void prohibeixAlCiutadaAccedirAlPanellDOperaris() throws Exception {
mockMvc.perform(get("/api/v1/intern/bicicletes")).andExpect(status().isForbidden());
}
@Test
void retorna401SenseTokenEnLlocDeRedirigirAlFormulari() throws Exception {
mockMvc.perform(post("/api/v1/lloguers/9/finalitzar"))
.andExpect(status().isUnauthorized())
.andExpect(jsonPath("$.status").value(401)); // ProblemDetail, no HTML
}
}Una anotació pròpia evita repetir la mateixa configuració en trenta proves:
@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(username = "[email protected]", roles = "CIUTADA")
public @interface AmbCiutada { }
@Retention(RetentionPolicy.RUNTIME)
@WithMockUser(username = "[email protected]", roles = "OPERARI")
public @interface AmbOperari { }I la prova que tanca el cercle del mòdul 5: la regla de @PreAuthorize amb SeguretatLloguers.esPropietari. A 06-03 vam provar el bean en aïllament; el que falta és comprovar que Spring aplica l'anotació, i això exigeix el context amb la seguretat de mètode activa:
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
class SeguretatLloguersIT {
@Autowired private MockMvc mockMvc;
@Test
@WithUserDetails("[email protected]") // CIUTADA, id 7, carregat de veritat
void elCiutadaNoPotFinalitzarElLloguerDunAltre() throws Exception {
// El lloguer 9 pertany al ciutadà 12, no a la Marta
mockMvc.perform(post("/api/v1/lloguers/9/finalitzar")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"estacioDestiId\": 2}"))
.andExpect(status().isForbidden())
.andExpect(jsonPath("$.title").value("Accés denegat"));
}
@Test
@WithUserDetails("[email protected]")
void elCiutadaSiPotFinalitzarElSeu() throws Exception {
mockMvc.perform(post("/api/v1/lloguers/4/finalitzar") // el 4 sí que és de la Marta
.contentType(MediaType.APPLICATION_JSON)
.content("{\"estacioDestiId\": 2}"))
.andExpect(status().isOk());
}
}Aquestes dues proves són la resposta a la pregunta que obria el mòdul. El forat que 05-05 va tancar queda ara vigilat per assercions que s'executen a cada construcció; si algú esborra l'anotació, reordena l'expressió SpEL o reanomena el bean, la suite es posa en vermell. I fixem-nos en el parell: una prova de denegació i una de permís. Sense la segona, una regla que denegués sempre —la fallada més comuna en escriure SpEL, que no comprova el compilador— passaria desapercebuda.
Sobre csrf(): CicloUrbana el va desactivar justificadament a 05-02 per ser una API sense sessió, així que aquí no cal. En una aplicació amb sessió, un POST sense .with(csrf()) retorna 403 i desconcerta; aquest 403 és el senyal que falta el testimoni, no un problema de rols.
- Configuració específica de proves
# src/test/resources/application-test.yml
spring:
datasource:
url: jdbc:h2:mem:ribalta;DB_CLOSE_DELAY=-1
jpa:
hibernate:
ddl-auto: create-drop # en proves sí; en producció, validate (04-08)
show-sql: true
flyway:
enabled: false # l'esquema el crea Hibernate en aquesta llesca
ciclourbana:
xarxa:
capacitat-minima: 8
llindar-bateria: 20
jwt:
secret: clau-de-prova-de-com-a-minim-43-caracters-base64-per-hs256
expiracio-acces: 15m
logging:
level:
org.springframework.security: DEBUGRecorda de 06-01: el fitxer s'anomena application-test.yml, no application.yml. Un application.yml a src/test/resources no es fusiona amb el de producció, el substitueix.
| Mecanisme | Abast | Efecte a la memòria cau |
|---|---|---|
@ActiveProfiles("test") |
Classe | Nova clau per combinació de perfils |
@TestPropertySource(properties = "...") |
Classe | Nova clau per joc de propietats |
@DynamicPropertySource |
Classe, valors calculats en execució | Igual; és la porta a Testcontainers (06-05) |
@TestConfiguration + @Import |
Classe | Nova clau |
@DynamicPropertySource mereix una mirada, perquè és el que farà possible la lliçó següent: permet calcular propietats després que arrenqui alguna cosa externa però abans que es creï el context.
@DynamicPropertySource
static void propietatsDinamiques(DynamicPropertyRegistry registre) {
registre.add("ciclourbana.notificacions.url", servidorFals::getUrl);
}I @TestConfiguration aporta beans només per a les proves que la importin —a diferència de @Configuration, que seria recollida per l'escaneig i afectaria tot:
@TestConfiguration
public class ConfiguracioRellotgeFix {
@Bean
@Primary // guanya al Clock de ConfiguracioComuna
public Clock rellotgeCongelat() {
return Clock.fixed(Instant.parse("2026-03-14T09:00:00Z"), ZoneOffset.UTC);
}
}Importar-la amb @Import(ConfiguracioRellotgeFix.class) fa determinista qualsevol prova d'integració que depengui del temps, amb la mateixa idea de 06-02 però a escala de context.
- Dades amb
@Sql i ApplicationContextRunner
@Sql i ApplicationContextRunner@Sql executa guions abans o després d'una prova:
@Test
@Sql(scripts = "/dades/estacions-ribalta.sql") // abans
@Sql(scripts = "/dades/netejar.sql", executionPhase = AFTER_TEST_METHOD)
void llistaLesEstacionsCarregadesPelGuio() throws Exception { /* ... */ }Posada sobre la classe s'aplica a tots els seus mètodes, i diversos guions s'executen en ordre. Davant de preparar les dades amb el TestEntityManager o els repositoris, @Sql guanya quan el joc de dades és gran o quan cal un estat que les entitats no permeten construir; perd en el fet que el guió es desincronitza de l'esquema així que arriba una migració nova. Per a CicloUrbana la regla és: dades petites i expressives amb TestEntityManager i les fàbriques de 06-02; jocs grans i estables amb @Sql.
I per a l'altre extrem de l'espectre, ApplicationContextRunner —que ja va aparèixer a 02-06— prova la configuració mateixa sense arrencar cap context complet, en mil·lisegons:
@Test
void lArrencadaFallaSiFaltaElSecretJwt() {
new ApplicationContextRunner()
.withUserConfiguration(ConfiguracioJwt.class)
.run(context -> assertThat(context)
.hasFailed()
.getFailure()
.hasMessageContaining("jwt.secret"));
}
@Test
void registraLaTarifaJubilatAutomaticamentEnSerAlClasspath() {
new ApplicationContextRunner()
.withUserConfiguration(TarifaEstandard.class, TarifaEstudiant.class,
TarifaJubilat.class, SelectorTarifa.class)
.run(context -> assertThat(context.getBean(SelectorTarifa.class).disponibles())
.containsKeys("estandard", "estudiant", "jubilat"));
}
@Test
void noRegistraElStarterQuanLaPropietatEstaDesactivada() {
new ApplicationContextRunner()
.withPropertyValues("ciclourbana.xarxa.enabled=false")
.withConfiguration(AutoConfigurations.of(XarxaAutoConfiguration.class))
.run(context -> assertThat(context).doesNotHaveBean(XarxaProperties.class));
}És l'eina ideal per verificar @ConditionalOnProperty, @ConditionalOnMissingBean i la validació de @ConfigurationProperties del mòdul 2: comprova comportament de context sense pagar l'arrencada ni fragmentar la memòria cau.
Errors Comuns i Consells
Fer servir @SpringBootTest per a tot. És el camí directe al con de gelat de 06-01. Abans d'escriure-ho, pregunta't quina capa estàs provant: gairebé sempre hi ha una llesca que n'hi ha prou i costa una desena part.
Continuar fent servir @MockBean. Està obsoleta des d'Spring Boot 3.4. Substitueix-la per @MockitoBean (i @SpyBean per @MockitoSpyBean); el canvi és mecànic i evita una migració forçosa més endavant.
Fragmentar la memòria cau de contextos sense adonar-se'n. Un @TestPropertySource posat «només per a aquesta prova» afegeix una arrencada completa. Abans d'afegir configuració a una classe, mira si pot viure al perfil test comú.
Oblidar em.flush() i em.clear() a @DataJpaTest. La consulta es respon des de la memòria cau de primer nivell i la prova passa encara que el SQL sigui incorrecte. És el fals positiu més perillós d'aquesta lliçó.
Posar application.yml a src/test/resources. Substitueix el de producció en lloc de complementar-lo. Fes servir application-test.yml amb @ActiveProfiles("test").
@WebMvcTest sense @Import de la configuració de seguretat. Les proves passen perquè no hi ha regles a aplicar, cosa que dona una falsa sensació de cobertura de seguretat.
Escriure només proves de denegació. Comprovar els 403 sense comprovar els 200 deixa passar una regla que denega sempre, que és la fallada més comuna de SpEL. Cada regla necessita el seu parell.
Consell: una classe base per a les proves d'integració. Concentra @SpringBootTest, @ActiveProfiles("test") i la configuració comuna en un sol lloc: una clau de memòria cau, una arrencada, i un únic punt on 06-05 endollarà el contenidor de PostgreSQL.
Consell: .andDo(print()) és la teva primera eina. Davant d'una fallada incomprensible a MockMvc, abocar petició i resposta resol la majoria dels casos en trenta segons.
Consell: anomena les proves d'integració *IT. Failsafe les separa de les ràpides (06-01), així ./mvnw test continua durant segons i ./mvnw verify ho comprova tot.
Exercicis
Exercici 1
Escriu EstacioControllerTest complet amb @WebMvcTest cobrint quatre casos del contracte públic: llistat paginat que retorna un PaginaResponse amb els seus camps contingut, pagina i totalElements; el 404 amb ProblemDetail d'una estació inexistent; el 400 en crear amb capacitat -5, verificant que el servei no s'ha arribat a invocar; i el 409 amb codi ESTACIO_DUPLICADA quan el servei llança EstacioDuplicadaException. Explica per què les quatre són llesques i cap no necessita @SpringBootTest.
Exercici 2
Crea l'anotació @AmbCiutada de l'apartat 10 i escriu la matriu d'accés de CicloUrbana com a proves: per a les rutes GET /api/v1/estacions, POST /api/v1/estacions, GET /api/v1/lloguers i GET /api/v1/intern/bicicletes, comprova el resultat esperat per a anònim, ciutadà, operari i administrador. Dissenya la prova de manera que afegir una ruta nova costi una línia, no un mètode. Pista: @ParameterizedTest amb @CsvSource i SecurityMockMvcRequestPostProcessors.user(...).
Exercici 3
Un company informa que la suite ha passat de 45 segons a 6 minuts després d'afegir dotze classes de prova. El registre de la memòria cau diu [size = 11, hitCount = 3, missCount = 11]. Diagnostica el problema, explica què signifiquen aquests números i proposa un pla concret de tres passos per tornar a un sol context compartit.
Solucions
Solució 1
@WebMvcTest(EstacioController.class)
class EstacioControllerTest {
@Autowired private MockMvc mockMvc;
@MockitoBean private EstacioService estacioService;
@Test
void retornaElLlistatPaginatAmbLesSevesMetadades() throws Exception {
when(estacioService.llistar(any())).thenReturn(new PaginaResponse<>(
List.of(new EstacioResponse(1L, "Plaça Major", "Plaça Major, 1", 24, 9)),
0, 20, 4L, 1, true, true));
mockMvc.perform(get("/api/v1/estacions?page=0&size=20"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.contingut", hasSize(1)))
.andExpect(jsonPath("$.contingut[0].nom").value("Plaça Major"))
.andExpect(jsonPath("$.totalElements").value(4));
}
@Test
void retorna404AmbProblemDetailQuanNoExisteix() throws Exception {
when(estacioService.obtenirPerId(99L))
.thenThrow(new RecursNoTrobatException("Estació", 99L));
mockMvc.perform(get("/api/v1/estacions/99"))
.andExpect(status().isNotFound())
.andExpect(content().contentType(MediaType.APPLICATION_PROBLEM_JSON))
.andExpect(jsonPath("$.status").value(404));
}
@Test
void retorna400SenseCridarElServeiQuanLaCapacitatEsNegativa() throws Exception {
mockMvc.perform(post("/api/v1/estacions").contentType(MediaType.APPLICATION_JSON)
.content("{\"nom\":\"X\",\"adreca\":\"C/1\",\"capacitat\":-5}"))
.andExpect(status().isBadRequest());
verifyNoInteractions(estacioService);
}
@Test
void retorna409AmbElCodiDeNegociQuanElNomEstaDuplicat() throws Exception {
when(estacioService.crear(any())).thenThrow(new EstacioDuplicadaException("Plaça Major", 1L));
mockMvc.perform(post("/api/v1/estacions").contentType(MediaType.APPLICATION_JSON)
.content("{\"nom\":\"Plaça Major\",\"adreca\":\"C/1\",\"capacitat\":24}"))
.andExpect(status().isConflict())
.andExpect(jsonPath("$.codi").value("ESTACIO_DUPLICADA"));
}
}Per què cap no necessita @SpringBootTest: les quatre verifiquen exclusivament el que viu a la capa web —mapatge de rutes, deserialització, validació declarativa, serialització de la resposta, codis d'estat i el @RestControllerAdvice—, i tot això ho carrega @WebMvcTest. El que hi ha per sota està simulat expressament: la lògica d'EstacioService ja es prova en unitàries (06-02) i la persistència es provarà amb @DataJpaTest i Testcontainers. Aixecar JPA, Hikari, Flyway i la seguretat per comprovar un jsonPath seria pagar quatre segons per prova sense detectar ni una fallada més.
Solució 2
@WebMvcTest
@Import(ConfiguracioSeguretat.class)
class MatriuDAccesTest {
@Autowired private MockMvc mockMvc;
@MockitoBean private EstacioService estacioService;
@MockitoBean private LloguerService lloguerService;
@MockitoBean private ServeiDetallsUsuari serveiDetallsUsuari;
@ParameterizedTest(name = "{0} {1} com a {2} -> {3}")
@CsvSource({
"GET, /api/v1/estacions, ANONIM, 200",
"GET, /api/v1/estacions, CIUTADA, 200",
"POST, /api/v1/estacions, CIUTADA, 403",
"POST, /api/v1/estacions, OPERARI, 403",
"POST, /api/v1/estacions, ADMIN, 201",
"GET, /api/v1/lloguers, ANONIM, 401",
"GET, /api/v1/lloguers, CIUTADA, 200",
"GET, /api/v1/intern/bicicletes, CIUTADA, 403",
"GET, /api/v1/intern/bicicletes, OPERARI, 200",
"GET, /api/v1/intern/bicicletes, ADMIN, 200"
})
void aplicaLaMatriuDAccesDeCicloUrbana(String verb, String ruta,
String rol, int esperat) throws Exception {
var peticio = "POST".equals(verb)
? post(ruta).contentType(MediaType.APPLICATION_JSON).content(COS_VALID)
: get(ruta);
if (!"ANONIM".equals(rol)) {
peticio = peticio.with(user("[email protected]").roles(rol));
}
mockMvc.perform(peticio).andExpect(status().is(esperat));
}
}Comentari: la taula ÉS l'especificació de seguretat, i afegir una ruta costa exactament una línia. Es fa servir .with(user(...)) en lloc de @WithMockUser perquè el rol és un paràmetre i una anotació no el pot rebre. Cal notar que l'if que decideix si afegir l'usuari no és la «lògica condicional en una prova» que 06-02 prohibia: no tria què comprovar —l'asserció és sempre la mateixa— sinó com construir l'escenari que la fila descriu. La línia que separa totes dues coses és si l'if afecta les assercions.
Un detall final de disseny: hi apareixen les files de 200 i 201, no només les de 401 i 403. Sense elles, una configuració que denegués absolutament tot passaria la meitat de la taula en verd.
Solució 3
Què signifiquen els números. size = 11 són onze contextos diferents en memòria cau; missCount = 11, onze arrencades completes; hitCount = 3, només tres reutilitzacions. Traduït: cada classe de prova està aixecant el seu propi context. Amb quatre segons d'arrencada, aquí hi ha els cinc minuts i mig de més.
Diagnòstic. No és que les proves siguin lentes: és que les seves claus de memòria cau són totes diferents. Les causes habituals, en ordre de freqüència: cada classe declara un @MockitoBean diferent; alguna afegeix @TestPropertySource amb una propietat concreta; hi ha perfils diferents (@ActiveProfiles("test") en unes, res o "dev" en altres); i algun @DirtiesContext heretat destrueix el context després de cada classe.
Pla de tres passos:
- Crear una classe base comuna amb
@SpringBootTest,@ActiveProfiles("test")i@AutoConfigureMockMvc, i fer que les dotze n'heretin. Només això sol reduir onze contextos a dos o tres. - Moure a
application-test.ymltota propietat que avui estigui en un@TestPropertySource, i eliminar els@DirtiesContextque no estiguin justificats per escrit. Si una prova embruta el context, arreglar la prova —netejant el que crea— és més barat que reconstruir el context sencer. - Reclassificar. Revisar les dotze i preguntar-se quines necessiten de veritat el context complet: les que només proven la capa web passen a
@WebMvcTest, les de consultes a@DataJpaTest, i les que proven configuració condicional aApplicationContextRunner. Les que sobrevisquin com a@SpringBootTestcompartiran context gràcies al pas 1.
I la comprovació objectiva que ha funcionat: tornar a mirar el registre. L'objectiu és size de 2 o 3 i un hitCount molt superior al missCount.
Conclusió
CicloUrbana ja no confia: comprova. Saps què afegeix una prova d'integració sobre les unitàries —el cablejat, les anotacions i el comportament del framework, que cap mock no pot veure— i què no s'hi ha de fer. Coneixes @SpringBootTest i els seus quatre entorns web, amb MOCK com a opció per defecte i RANDOM_PORT reservat per als pocs camins que mereixen un Tomcat de veritat. I, sobretot, entens la memòria cau de contextos: què la forma, què la invalida —@MockitoBean, @TestPropertySource, @ActiveProfiles, @DirtiesContext— i per què una classe base comuna i un perfil test compartit són la diferència entre una suite de quaranta segons i una de sis minuts que ningú no executa.
Domines les llesques: @WebMvcTest amb MockMvc per al contracte públic —codis d'estat, capçalera Location, jsonPath, el ProblemDetail de 03-06, la validació de 03-04 i l'asserció de contracte que impedeix que un camp intern es filtri—, @DataJpaTest amb la seva base incrustada, el seu rollback automàtic i el parany de la memòria cau de primer nivell que fa passar proves amb SQL incorrecte si falta flush i clear, més @JsonTest, @RestClientTest i les variants JDBC. Saps que @MockBean està obsoleta des d'Spring Boot 3.4 i que la seva substituta és @MockitoBean, i coneixes MockMvcTester com l'alternativa fluida i moderna a l'API clàssica.
I has convertit en assercions automàtiques tot el mòdul 5: spring-security-test amb @WithMockUser, @WithAnonymousUser, @WithUserDetails, l'anotació pròpia @AmbCiutada i els post-processors user(...), jwt() i csrf(); la matriu d'accés de 05-02 escrita com una taula parametritzada on afegir una ruta costa una línia; i les dues proves que tanquen el cercle del curs fins aquí: la Marta rep un 403 en intentar finalitzar el lloguer aliè i un 200 en finalitzar el seu, amb el parell complet, perquè una regla que denega sempre és la fallada més freqüent de SpEL. Saps configurar el perfil test, calcular propietats en execució amb @DynamicPropertySource, aportar beans només de prova amb @TestConfiguration —inclòs el Clock congelat a escala de context—, carregar dades amb @Sql i verificar la configuració condicional del mòdul 2 amb ApplicationContextRunner sense pagar ni una sola arrencada.
Queda una escletxa, i és gran. Tot el que s'ha provat contra la base de dades en aquesta lliçó ha corregut sobre H2 en memòria, i producció és PostgreSQL 16. No són el mateix: difereixen en dialecte, en tipus, en funcions, en el comportament de les seqüències i en el que accepten com a SQL natiu. La consulta nativa de 04-06 que H2 no entén, l'índex parcial de la migració V3, la validació de ddl-auto: validate contra l'esquema real que deixen V1…V6 de 04-08: res d'això no està comprovat, i algunes d'aquestes proves passen avui en verd mentre el codi fallaria a Ribalta. La lliçó següent, Proves amb Testcontainers, aixeca un PostgreSQL real i efímer a Docker per a cada suite, el connecta amb @ServiceConnection i converteix aquesta última zona de fe en una zona d'assercions.
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
