Vai al contenuto

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);
}
  1. @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

  1. Aggiungi un test che verifica version (optimistic lock) incrementarsi a ogni update.
  2. Estrai una Fixtures factory class per costruire Project e Task di test.
  3. Configura testcontainers.reuse.enable=true in ~/.testcontainers.properties e misura lo speedup.

Riferimenti nel codice