Step 07 — Testing: unit + Testcontainers¶
Obiettivo: una piramide di test sana.
Strategia¶
| Livello | Tecnologia | Velocità | Cosa testa |
|---|---|---|---|
| Domain unit | JUnit + AssertJ | <50 ms | Regole intrinseche (state machine, validation) |
| Service unit | Mockito + JUnit | <100 ms | Logica con repository mockato |
| Slice test | @WebMvcTest, @DataJpaTest | <2 s | Layer isolato + Spring context minimo |
| Integration | Testcontainers + Spring Boot Test | 5–15 s | Sistema reale con PostgreSQL effimero |
Unit test (dominio)¶
TaskStatusTest.java
@ParameterizedTest
@CsvSource({
"TODO, IN_PROGRESS, true",
"TODO, DONE, true",
"DONE, IN_PROGRESS, false"
})
void transitionsRespectStateMachine(TaskStatus from, TaskStatus to, boolean allowed) {
assertThat(from.canTransitionTo(to)).isEqualTo(allowed);
}
Service unit con Mockito¶
ProjectServiceTest.java
@ExtendWith(MockitoExtension.class)
class ProjectServiceTest {
@Mock ProjectRepository repository;
ProjectMapper mapper = new ProjectMapper();
ProjectService service;
@BeforeEach
void setUp() { service = new ProjectService(repository, mapper); }
@Test
void getThrowsWhenMissing() {
UUID id = UUID.randomUUID();
when(repository.findById(id)).thenReturn(Optional.empty());
assertThatThrownBy(() -> service.get(id))
.isInstanceOf(ResourceNotFoundException.class);
}
}
Integration test con Testcontainers¶
AbstractIntegrationTest.java
@SpringBootTest(webEnvironment = RANDOM_PORT)
@ActiveProfiles("test")
@Testcontainers
public abstract class AbstractIntegrationTest {
@Container
@ServiceConnection // (1)
static final PostgreSQLContainer<?> POSTGRES =
new PostgreSQLContainer<>("postgres:16-alpine")
.withReuse(true);
}
@ServiceConnection(Spring Boot 3.1+) — niente proprietà manuali, Spring legge l'URL del container.
ProjectControllerIT.java (estratto)
@AutoConfigureMockMvc
class ProjectControllerIT extends AbstractIntegrationTest {
@Autowired MockMvc mockMvc;
@Test
void createListGetArchiveAndDelete() throws Exception {
mockMvc.perform(post("/api/v1/projects")
.contentType(APPLICATION_JSON)
.content("{\"name\":\"API redesign\"}"))
.andExpect(status().isCreated())
.andExpect(header().exists("Location"));
}
}
Perché PostgreSQL reale e non H2¶
| Problema | H2 | Testcontainers (Postgres) |
|---|---|---|
| SQL dialect | diverso da prod | identico a prod |
| Tipi (UUID, jsonb, …) | finti / mancanti | nativi |
| Migration Flyway | spesso falliscono | OK |
| Reproducibility | "funziona su H2 ma non in prod" | "se passa qui, passa in prod" |
Esercizi¶
- Aggiungi un test che verifica
version(optimistic lock) incrementarsi a ogni update. - Estrai una
Fixturesfactory class per costruireProjecteTaskdi test. - Configura
testcontainers.reuse.enable=truein~/.testcontainers.propertiese misura lo speedup.