Spring Boot 4 testing strategies and patterns. Use when writing unit tests, slice tests (@WebMvcTest, @DataJpaTest), integration tests, Testcontainers with @ServiceConnection, security testing (@WithMockUser, JWT), or Modulith event testing with Scenario API. Covers the critical @MockitoBean migration from @MockBean.
This skill inherits all available tools. When active, it can use any tool Claude has access to.
references/modulith-testing.mdreferences/security-testing.mdreferences/slice-tests.mdreferences/testcontainers.mdComprehensive testing patterns including slice tests, Testcontainers, security testing, and Modulith Scenario API.
| Old (Boot 3.x) | New (Boot 4.x) | Notes |
|---|---|---|
@MockBean | @MockitoBean | Required migration |
@SpyBean | @MockitoSpyBean | Required migration |
Implicit @AutoConfigureMockMvc | Explicit annotation required | Add to @SpringBootTest |
| Test Type | Annotation | Use When |
|---|---|---|
| Controller | @WebMvcTest | Testing request/response, validation |
| Repository | @DataJpaTest | Testing queries, entity mapping |
| JSON | @JsonTest | Testing serialization/deserialization |
| REST Client | @RestClientTest | Testing external API clients |
| Full Integration | @SpringBootTest | End-to-end, with real dependencies |
| Module | @ApplicationModuleTest | Testing bounded context in isolation |
@MockitoBean for external services@ServiceConnection for databases@WithMockUser, JWT mocking@WebMvcTest(OrderController.class)
class OrderControllerTest {
@Autowired
private MockMvcTester mvc; // New in Boot 4
@MockitoBean // Replaces @MockBean
private OrderService orderService;
@Test
void shouldReturnOrder() {
given(orderService.findById(1L))
.willReturn(Optional.of(new Order(1L, "SUBMITTED")));
assertThat(mvc.get().uri("/api/orders/1"))
.hasStatusOk()
.hasContentType(MediaType.APPLICATION_JSON)
.bodyJson()
.extractingPath("$.status").isEqualTo("SUBMITTED");
}
@Test
void shouldReturn404WhenNotFound() {
given(orderService.findById(999L)).willReturn(Optional.empty());
assertThat(mvc.get().uri("/api/orders/999"))
.hasStatus(HttpStatus.NOT_FOUND);
}
}
@WebMvcTest(OrderController::class)
class OrderControllerTest(@Autowired val mvc: MockMvcTester) {
@MockitoBean
lateinit var orderService: OrderService
@Test
fun `should return order`() {
given(orderService.findById(1L))
.willReturn(Optional.of(Order(1L, "SUBMITTED")))
assertThat(mvc.get().uri("/api/orders/1"))
.hasStatusOk()
.bodyJson()
.extractingPath("$.status").isEqualTo("SUBMITTED")
}
}
@DataJpaTest
class OrderRepositoryTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private OrderRepository orderRepository;
@Test
void shouldFindOrdersWithItems() {
Order order = new Order(LocalDateTime.now());
order.addItem(new OrderItem("Widget", 2));
entityManager.persistAndFlush(order);
entityManager.clear(); // Force re-fetch
Order found = orderRepository.findById(order.getId()).orElseThrow();
assertThat(found.getItems()).hasSize(1);
}
}
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@Testcontainers
class OrderIntegrationTest {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");
@Container
@ServiceConnection
static RedisContainer redis = new RedisContainer("redis:7");
@Autowired
private WebTestClient webClient;
@Test
void shouldCreateOrder() {
webClient.post()
.uri("/api/orders")
.bodyValue(new CreateOrderRequest("Widget", 5))
.exchange()
.expectStatus().isCreated()
.expectBody()
.jsonPath("$.id").isNotEmpty();
}
}
@WebMvcTest(AdminController.class)
class AdminControllerSecurityTest {
@Autowired
private MockMvcTester mvc;
@Test
void shouldRejectUnauthenticated() {
assertThat(mvc.get().uri("/api/admin/users"))
.hasStatus(HttpStatus.UNAUTHORIZED);
}
@Test
@WithMockUser(roles = "USER")
void shouldRejectNonAdmin() {
assertThat(mvc.get().uri("/api/admin/users"))
.hasStatus(HttpStatus.FORBIDDEN);
}
@Test
@WithMockUser(roles = "ADMIN")
void shouldAllowAdmin() {
assertThat(mvc.get().uri("/api/admin/users"))
.hasStatusOk();
}
}
@ApplicationModuleTest
class OrderModuleTest {
@Autowired
private OrderService orderService;
@Test
void shouldPublishOrderCreatedEvent(Scenario scenario) {
scenario.stimulate(() -> orderService.createOrder(request))
.andWaitForEventOfType(OrderCreated.class)
.toArriveAndVerify(event -> {
assertThat(event.orderId()).isNotNull();
assertThat(event.customerId()).isEqualTo("customer-123");
});
}
}
| Anti-Pattern | Fix |
|---|---|
Using @MockBean in Boot 4 | Replace with @MockitoBean |
@SpringBootTest for unit tests | Use appropriate slice annotation |
Missing entityManager.clear() | Add to verify lazy loading |
| High-cardinality test data | Use minimal, focused fixtures |
| Shared mutable test state | Use @DirtiesContext or fresh containers |
| No security tests | Add @WithMockUser tests for endpoints |
@MockBean removed in Boot 4@DynamicPropertySource