The full picture
// 1. Unit — fast, no Spring: the RULES
class PricingPolicyTest {
@Test void bulkDiscountAppliesOver100Units() {
assertThat(new PricingPolicy().price(order(150)))
.isEqualTo(Money.of("1350.00"));
}
}
// 2. Slice — the CONTRACT at the web boundary
@WebMvcTest(OrderController.class)
class OrderControllerTest {
@Autowired MockMvcTester mvc;
@MockitoBean OrderService service; // collaborators mocked
@Test void invalidBodyYields422WithProblemDetail() {
assertThat(mvc.post().uri("/orders").contentType(APPLICATION_JSON)
.content("{}"))
.hasStatus(422)
.bodyJson().extractingPath("$.violations").isNotEmpty();
}
}
// 3. Integration — the WIRING, real infra
@SpringBootTest(webEnvironment = RANDOM_PORT)
@Testcontainers
class OrderFlowIT { … }- What slices load:
@WebMvcTest= controllers, converters, advice, security filters — no services/repos;@DataJpaTest= repositories, EntityManager, a transaction rolled back per test — no web. Fast because small; honest because they use the real framework plumbing you'd otherwise mock badly. - What only @SpringBootTest catches: bean wiring mistakes, property binding, security config integration, cross-cutting behavior (transactions + events + listeners together).
- Ratio heuristic: if the suite takes >5 min, count your @SpringBootTests; if domain rules are tested through HTTP, your unit layer is missing.
- Don't test the framework: asserting that Spring injects a bean or that JPA saves an entity is testing Spring, not your code — test your mappings, queries, and rules.