Spring BootSpringBootTest
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 { ... }