Spring Boot
SpringBootTest
SpringBootTest loads the full application context — all beans, configuration, security, web layer, and data layer — making it the closest approximation to a real running application. It is used for integration tests that verify the collaboration between multiple layers. Combined with Testcontainers for real databases and MockMvc or TestRestTemplate for HTTP, SpringBootTest catches issues that slice tests miss.
SpringBootTest Modes
@SpringBootTest has three web environment modes. MOCK (default) loads the web layer with a mock servlet environment — use with MockMvc. RANDOM_PORT starts a real embedded server on a random port — use with TestRestTemplate or WebTestClient. DEFINED_PORT starts a server on the port in application.properties. RANDOM_PORT is recommended for integration tests as it avoids port conflicts.
Java
// ── Mode 1: MOCK (default) — mock servlet, use MockMvc: ─────────────
@SpringBootTest
@AutoConfigureMockMvc
class UserIntegrationTestMockMvc {
@Autowired
private MockMvc mockMvc;
@Test
void findAll_returns200() throws Exception {
mockMvc.perform(get("/api/users")
.with(jwt().authorities(
new SimpleGrantedAuthority("ROLE_ADMIN"))))
.andExpect(status().isOk());
}
}
// ── Mode 2: RANDOM_PORT — real server, use TestRestTemplate: ──────────
@SpringBootTest(webEnvironment =
SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserIntegrationTestRestTemplate {
@Autowired
private TestRestTemplate restTemplate;
@LocalServerPort
private int port;
@Test
void findAll_authenticated_returns200() {
// TestRestTemplate with basic auth:
ResponseEntity<List> response = restTemplate
.withBasicAuth("admin", "password")
.getForEntity("/api/users", List.class);
assertThat(response.getStatusCode())
.isEqualTo(HttpStatus.OK);
}
}
// ── Mode 3: RANDOM_PORT with WebTestClient (reactive): ────────────────
@SpringBootTest(webEnvironment =
SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserIntegrationTestWebClient {
@Autowired
private WebTestClient webTestClient;
@Test
@WithMockUser(roles = "ADMIN")
void findAll_returns200() {
webTestClient.get().uri("/api/users")
.exchange()
.expectStatus().isOk()
.expectBodyList(UserResponse.class)
.hasSize(0);
}
}Full Integration Test with Testcontainers
A full integration test starts the entire application with a real database (PostgreSQL via Testcontainers) and exercises the complete request-to-database stack. These tests catch wiring issues, transaction boundaries, and SQL problems that neither slice tests nor unit tests find.
Java
@SpringBootTest(webEnvironment =
SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
@AutoConfigureMockMvc
class UserIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:16-alpine")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired private MockMvc mockMvc;
@Autowired private ObjectMapper objectMapper;
@Autowired private UserRepository userRepository;
@BeforeEach
void cleanDatabase() {
userRepository.deleteAll(); // clean slate for each test
}
// ── Create → read round-trip through all layers: ──────────────────
@Test
@WithMockUser(roles = "ADMIN")
void createUser_thenFindById_returnsCreatedUser() throws Exception {
CreateUserRequest request = new CreateUserRequest(
"Alice", "alice@example.com", "password123");
// Create:
String location = mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andReturn()
.getResponse()
.getHeader("Location");
// Extract ID from Location header:
Long id = Long.parseLong(
location.substring(location.lastIndexOf('/') + 1));
// Find by ID:
mockMvc.perform(get("/api/users/" + id))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Alice"))
.andExpect(jsonPath("$.email").value("alice@example.com"));
// Verify persisted in DB:
assertThat(userRepository.findById(id)).isPresent();
}
// ── Create → update → verify: ─────────────────────────────────────
@Test
@WithMockUser(roles = "ADMIN")
void updateUser_persistsChanges() throws Exception {
User saved = userRepository.save(User.builder()
.name("Bob").email("bob@example.com").build());
UpdateUserRequest update = new UpdateUserRequest("Robert", null);
mockMvc.perform(put("/api/users/" + saved.getId())
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(update)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Robert"));
assertThat(userRepository.findById(saved.getId()))
.isPresent()
.get()
.extracting(User::getName)
.isEqualTo("Robert");
}
// ── Delete → verify gone: ─────────────────────────────────────────
@Test
@WithMockUser(roles = "ADMIN")
void deleteUser_removesFromDatabase() throws Exception {
User saved = userRepository.save(User.builder()
.name("Charlie").email("charlie@example.com").build());
mockMvc.perform(delete("/api/users/" + saved.getId()))
.andExpect(status().isNoContent());
assertThat(userRepository.findById(saved.getId())).isEmpty();
}
}Mocking External Dependencies in SpringBootTest
A full SpringBootTest starts every bean including Feign clients that call external services. Use @MockBean to replace specific beans with Mockito mocks — the rest of the application context remains real. This allows testing the full stack while controlling external service responses.
Java
@SpringBootTest(webEnvironment =
SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
@AutoConfigureMockMvc
class OrderIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:16-alpine");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired private MockMvc mockMvc;
@Autowired private ObjectMapper objectMapper;
@Autowired private OrderRepository orderRepository;
// Real OrderService + OrderRepository, but mocked external clients:
@MockBean private UserClient userClient;
@MockBean private InventoryClient inventoryClient;
@MockBean private PaymentClient paymentClient;
@BeforeEach
void setUp() {
orderRepository.deleteAll();
// Stub external service responses:
when(userClient.findById(1L))
.thenReturn(UserResponse.builder()
.id(1L).name("Alice").email("alice@example.com").build());
when(inventoryClient.checkStock(10L))
.thenReturn(InventoryResponse.builder()
.productId(10L).available(true).stock(5).build());
when(paymentClient.charge(any()))
.thenReturn(PaymentResponse.builder()
.status(PaymentStatus.SUCCESS).build());
}
@Test
@WithMockUser(username = "1", roles = "USER")
void placeOrder_callsAllServices_persistsOrder() throws Exception {
CreateOrderRequest request = CreateOrderRequest.builder()
.userId(1L).productId(10L).quantity(2).build();
mockMvc.perform(post("/api/orders")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.status").value("CONFIRMED"));
// Verify all external services were called:
verify(userClient).findById(1L);
verify(inventoryClient).checkStock(10L);
verify(paymentClient).charge(any());
// Verify order was actually persisted:
assertThat(orderRepository.findAll()).hasSize(1);
}
@Test
@WithMockUser(username = "1", roles = "USER")
void placeOrder_insufficientStock_returns409() throws Exception {
when(inventoryClient.checkStock(10L))
.thenReturn(InventoryResponse.builder()
.productId(10L).available(false).stock(0).build());
CreateOrderRequest request = CreateOrderRequest.builder()
.userId(1L).productId(10L).quantity(2).build();
mockMvc.perform(post("/api/orders")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isConflict())
.andExpect(jsonPath("$.message")
.value(containsString("out of stock")));
assertThat(orderRepository.findAll()).isEmpty();
}
}Application Properties for Tests
Tests should use a separate configuration profile to override production settings — pointing to test databases, disabling scheduled tasks, enabling SQL logging, and reducing security overhead. Spring Boot loads application-test.yml automatically when the test profile is active.
yaml
// ── src/test/resources/application-test.yml: ─────────────────────────
// spring:
// datasource:
// url: jdbc:h2:mem:testdb # overridden by @DynamicPropertySource
// # when using Testcontainers
// jpa:
// show-sql: true
// properties:
// hibernate:
// format_sql: true
// flyway:
// enabled: true # run migrations on test DB too
//
// logging:
// level:
// org.springframework.web: DEBUG
// org.hibernate.SQL: DEBUG
//
// # Disable scheduled jobs during tests:
// spring:
// task:
// scheduling:
// enabled: false
//
// # Use faster BCrypt cost for test speed:
// security:
// bcrypt:
// strength: 4 # default 10 — tests run faster with 4
// ── Activate test profile on every test: ─────────────────────────────
@SpringBootTest
@ActiveProfiles("test")
class BaseIntegrationTest { ... }
// ── Or set in src/test/resources/application.properties: ──────────────
// spring.profiles.active=test
// ── @TestPropertySource for per-class overrides: ─────────────────────
@SpringBootTest
@TestPropertySource(properties = {
"spring.kafka.consumer.auto-offset-reset=earliest",
"feign.circuitbreaker.enabled=false", // disable CB in this test
"app.feature.new-checkout=true" // enable feature flag
})
class CheckoutFeatureFlagTest { ... }