Contents

Sliced test annotations load only a subset of the application context, making tests much faster than @SpringBootTest while still exercising the real Spring infrastructure for that layer.

// @WebMvcTest — loads only MVC layer: controllers, filters, @ControllerAdvice // Does NOT start a real server, does NOT load @Service or @Repository beans @WebMvcTest(OrderController.class) class OrderControllerTest { @Autowired MockMvc mvc; @MockBean OrderService orderService; // must be provided — not loaded by slice @Test void getOrder_returns200() throws Exception { given(orderService.findById(1L)).willReturn(Order.of(1L, "PENDING")); mvc.perform(get("/orders/1").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.status").value("PENDING")); } @Test void getOrder_notFound_returns404() throws Exception { given(orderService.findById(99L)).willThrow(new OrderNotFoundException(99L)); mvc.perform(get("/orders/99")) .andExpect(status().isNotFound()); } } // @DataJpaTest — loads JPA layer only: entities, repositories, datasource // Rolls back each test by default, uses H2 unless you override @DataJpaTest @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) // use real DB @Import(TestcontainersConfig.class) // bring in Testcontainers PostgreSQL (see below) class ProductRepositoryTest { @Autowired ProductRepository repo; @Test void findByCategory_returnsMatchingProducts() { repo.save(new Product("Widget", "ELECTRONICS", 9.99)); repo.save(new Product("Gadget", "ELECTRONICS", 19.99)); repo.save(new Product("Book", "BOOKS", 4.99)); List<Product> electronics = repo.findByCategory("ELECTRONICS"); assertThat(electronics).hasSize(2); } } // @JsonTest — loads only Jackson / JSON serialization @JsonTest class OrderDtoJsonTest { @Autowired JacksonTester<OrderDto> json; @Test void serialize() throws Exception { OrderDto dto = new OrderDto(1L, "PENDING", BigDecimal.valueOf(99.99)); assertThat(json.write(dto)).hasJsonPathNumberValue("$.id", 1); assertThat(json.write(dto)).extractingJsonPathStringValue("$.status").isEqualTo("PENDING"); } @Test void deserialize() throws Exception { String content = """ {"id":1,"status":"PENDING","total":99.99} """; assertThat(json.parse(content).getObject().status()).isEqualTo("PENDING"); } }
// @MockBean — replaces the Spring bean with a Mockito mock // Use when: you want full control over a dependency and don't need any real behaviour @WebMvcTest(CheckoutController.class) class CheckoutControllerTest { @MockBean PaymentService paymentService; // complete mock — all methods return null/0/false @MockBean InventoryService inventoryService; @Test void checkout_chargesPayment() throws Exception { given(inventoryService.reserve(any())).willReturn(true); given(paymentService.charge(any())).willReturn(PaymentResult.success("txn-123")); mvc.perform(post("/checkout").contentType(APPLICATION_JSON).content(""" {"cartId":"cart-1","paymentToken":"tok_visa"} """)) .andExpect(status().isOk()) .andExpect(jsonPath("$.transactionId").value("txn-123")); } } // @SpyBean — wraps a real Spring bean with Mockito spy // Use when: you want the real implementation but override only specific methods @SpringBootTest class NotificationServiceTest { @SpyBean EmailService emailService; // real EmailService, but we can stub/verify @Test void sendWelcome_callsEmailService() { // Only stub the method that would send a real email doNothing().when(emailService).send(any()); notificationService.sendWelcome("user@example.com"); verify(emailService).send(argThat(msg -> msg.to().equals("user@example.com") && msg.subject().contains("Welcome"))); } } // Plain Mockito (no Spring) — fastest for pure unit tests class OrderServiceTest { OrderRepository repo = Mockito.mock(OrderRepository.class); OrderService sut = new OrderService(repo); // constructor injection @Test void create_savesAndReturnsOrder() { Order saved = new Order(1L, "NEW"); when(repo.save(any())).thenReturn(saved); Order result = sut.create(new CreateOrderRequest("item-1")); assertThat(result.id()).isEqualTo(1L); } } Every @MockBean or @SpyBean in a test class causes Spring to create a new application context — destroying the cached context and slowing your suite. Group tests with identical context shapes into the same class, or use @TestConfiguration to avoid context pollution. // @TestConfiguration — defines beans available only in tests // NOT picked up by component scan unless @Import-ed explicitly @TestConfiguration public class TestcontainersConfig { @Bean @ServiceConnection // Spring Boot 3.1+ auto-wires connection properties PostgreSQLContainer<?> postgresContainer() { return new PostgreSQLContainer<>("postgres:16-alpine") .withReuse(true); } } // Import into a test class @DataJpaTest @Import(TestcontainersConfig.class) class UserRepositoryTest { /* ... */ } // Sharing a @TestConfiguration across multiple test classes // Create a base test class annotated with @SpringBootTest + @Import @SpringBootTest @Import(TestcontainersConfig.class) @ActiveProfiles("test") public abstract class AbstractIntegrationTest { // common test utilities } class OrderIT extends AbstractIntegrationTest { @Test void createOrder() { /* ... */ } }

Use WireMock to stub external HTTP services (payment gateways, third-party APIs, microservices). It starts a real HTTP server on a random port so your WebClient / RestTemplate / HttpClient code runs unchanged.

// Add dependency: org.springframework.cloud:spring-cloud-contract-wiremock // or: com.github.tomakehurst:wiremock-jre8-standalone @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureWireMock(port = 0) // Spring Cloud Contract WireMock — random port class PaymentClientTest { @Autowired PaymentClient paymentClient; // uses ${wiremock.server.port} property @Test void charge_success() { // Stub the external payment API stubFor(post(urlEqualTo("/v1/charges")) .withHeader("Authorization", matching("Bearer .*")) .withRequestBody(matchingJsonPath("$.amount")) .willReturn(aResponse() .withStatus(200) .withHeader("Content-Type", "application/json") .withBody(""" {"id":"ch_123","status":"succeeded","amount":9999} """))); ChargeResult result = paymentClient.charge("tok_visa", 9999); assertThat(result.id()).isEqualTo("ch_123"); assertThat(result.status()).isEqualTo("succeeded"); verify(1, postRequestedFor(urlEqualTo("/v1/charges"))); } @Test void charge_networkError_throwsException() { stubFor(post(urlEqualTo("/v1/charges")) .willReturn(aResponse().withFault(Fault.CONNECTION_RESET_BY_PEER))); assertThatThrownBy(() -> paymentClient.charge("tok_bad", 100)) .isInstanceOf(PaymentException.class); } } // Standalone WireMock server (no Spring Cloud Contract) @ExtendWith(WireMockExtension.class) class ShippingClientTest { @RegisterExtension static WireMockExtension wm = WireMockExtension.newInstance() .options(wireMockConfig().dynamicPort()) .build(); ShippingClient client; @BeforeEach void setup() { client = new ShippingClient("http://localhost:" + wm.getPort()); } @Test void getRate_returnsRate() { wm.stubFor(get("/rates?from=10001&to=90210") .willReturn(okJson("""{"rate":12.99,"carrier":"UPS"}"""))); Rate rate = client.getRate("10001", "90210"); assertThat(rate.carrier()).isEqualTo("UPS"); } }
// Add: org.springframework.boot:spring-boot-testcontainers (Spring Boot 3.1+) // and: org.testcontainers:postgresql @SpringBootTest @Testcontainers class OrderRepositoryIntegrationTest { @Container @ServiceConnection // auto-configures spring.datasource.* from the container static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine"); @Autowired OrderRepository orderRepo; @Test @Transactional void saveAndFind() { Order order = orderRepo.save(new Order(null, "NEW", BigDecimal.TEN)); assertThat(order.id()).isNotNull(); Optional<Order> found = orderRepo.findById(order.id()); assertThat(found).isPresent(); assertThat(found.get().status()).isEqualTo("NEW"); } } // Useful container types new PostgreSQLContainer<>("postgres:16-alpine"); new MySQLContainer<>("mysql:8.3"); new MongoDBContainer("mongo:7"); new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.6.0")); new GenericContainer<>("redis:7-alpine").withExposedPorts(6379); new LocalStackContainer(DockerImageName.parse("localstack/localstack:3")) .withServices(S3, SQS);

Starting a new container for every test class is slow. Use the singleton pattern to share one container across the entire test suite.

// AbstractContainerBaseTest.java — shared by all integration tests public abstract class AbstractContainerBaseTest { static final PostgreSQLContainer<?> POSTGRES; static { POSTGRES = new PostgreSQLContainer<>("postgres:16-alpine") .withReuse(true); // requires testcontainers.reuse.enable=true in ~/.testcontainers.properties POSTGRES.start(); } } // Or with Spring Boot 3.1 @ServiceConnection in a shared @TestConfiguration: @TestConfiguration(proxyBeanMethods = false) public class SharedTestContainers { @Bean @ServiceConnection @Scope("singleton") PostgreSQLContainer<?> postgres() { return new PostgreSQLContainer<>("postgres:16-alpine"); } } # ~/.testcontainers.properties testcontainers.reuse.enable=true With withReuse(true), Testcontainers reuses a running container across test runs (identified by a hash of the container config). This dramatically speeds up local development — container startup is paid only once.

Spring's test framework caches application contexts keyed by their configuration. Understanding what breaks the cache is critical for fast test suites.

// These invalidate the context cache (each creates a NEW context): // 1. Different @MockBean / @SpyBean declarations // 2. @DirtiesContext on a test class or method // 3. Different @ActiveProfiles // 4. Different @TestPropertySource values // 5. Different @ContextConfiguration classes // ✅ Good — same context shared across all three test classes @SpringBootTest @ActiveProfiles("test") class OrderServiceIT { /* @MockBean PaymentService */ } @SpringBootTest @ActiveProfiles("test") class UserServiceIT { /* @MockBean PaymentService — SAME mock, context reused */ } // ❌ Bad — each class gets its own context @SpringBootTest class OrderServiceIT { @MockBean PaymentService p; } @SpringBootTest class UserServiceIT { @MockBean EmailService e; } // different beans → new context // ✅ Fix — collect all @MockBean declarations in one base class @SpringBootTest @ActiveProfiles("test") abstract class BaseIT { @MockBean PaymentService paymentService; @MockBean EmailService emailService; } class OrderServiceIT extends BaseIT { } class UserServiceIT extends BaseIT { }