@SpringBootTest Annotation: Full Guide for Spring Boot Integration Tests

@SpringBootTest Annotation: Full Guide for Spring Boot Integration Tests

@SpringBootTest loads the full application context, making it the go-to annotation for integration tests in Spring Boot. Understanding its webEnvironment options and context slicing alternatives can dramatically speed up your test suite while keeping coverage high.

Key Takeaways

@SpringBootTest loads the full context by default. This gives you real beans, real configurations, and realistic behavior — but at the cost of startup time.

webEnvironment controls how the web layer runs. MOCK (default), RANDOM_PORT, DEFINED_PORT, and NONE each serve different testing scenarios.

Slice annotations like @WebMvcTest and @DataJpaTest are faster. They load only part of the context, cutting startup time significantly for focused tests.

Context caching means startup cost is paid once per unique configuration. Spring caches application contexts across tests — don't modify beans unnecessarily or you'll bust the cache.

Combine @SpringBootTest with @ActiveProfiles to control environment. Using a test profile lets you swap out real services for mocks without changing production configuration.

Integration tests in Spring Boot have a reputation for being slow and brittle. Most of that reputation is earned by teams misusing @SpringBootTest — loading the full context when a slice would do, or not understanding what the annotation actually does under the hood. This guide covers everything you need to use @SpringBootTest effectively.

What @SpringBootTest Actually Does

When you annotate a test class with @SpringBootTest, Spring Boot:

  1. Searches for the main @SpringBootApplication class (walking up the package hierarchy)
  2. Loads the full ApplicationContext with all beans, auto-configurations, and property sources
  3. Makes those beans available for injection into your test class
@SpringBootTest
class OrderServiceIntegrationTest {

    @Autowired
    private OrderService orderService;

    @Autowired
    private OrderRepository orderRepository;

    @Test
    void shouldCreateOrderAndPersistToDatabase() {
        Order order = orderService.createOrder("customer-1", List.of("item-A", "item-B"));

        assertThat(order.getId()).isNotNull();
        assertThat(orderRepository.findById(order.getId())).isPresent();
    }
}

That's a real integration test — OrderService calls OrderRepository, which talks to a real (or in-memory) database. No mocking of the persistence layer.

The webEnvironment Options

The most important parameter to @SpringBootTest is webEnvironment. It has four values:

MOCK (default)

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
class MyTest { ... }

Loads the web ApplicationContext but with a mocked servlet environment. The embedded server does not start. Use this when you want to test with MockMvc without an actual HTTP server.

RANDOM_PORT

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class MyTest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void shouldReturnProductList() {
        ResponseEntity<List> response = restTemplate.getForEntity(
            "http://localhost:" + port + "/api/products",
            List.class
        );
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
    }
}

Starts a real embedded server on a random port. @LocalServerPort injects the actual port. Use this for full end-to-end HTTP tests, especially when testing filters, interceptors, or security configurations that only activate with a real server.

DEFINED_PORT

Same as RANDOM_PORT but uses the port defined in application.properties (default 8080). Rarely useful in automated tests — parallel test runs will conflict. Stick to RANDOM_PORT.

NONE

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
class MyTest { ... }

Loads an ApplicationContext with no web layer at all. Use this for testing pure service logic that has no web dependencies.

Loading Only Specific Parts of the Context

Running the full context for every test is expensive. Spring Boot provides test slices — pre-configured subsets of the application context.

@WebMvcTest — Controller Layer Only

@WebMvcTest(ProductController.class)
class ProductControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private ProductService productService;

    @Test
    void shouldReturn404WhenProductNotFound() throws Exception {
        when(productService.findById(99L)).thenReturn(Optional.empty());

        mockMvc.perform(get("/api/products/99"))
            .andExpect(status().isNotFound());
    }
}

Only loads the web layer: controllers, filters, ControllerAdvice, WebMvcConfigurer. Everything else must be @MockBeaned.

@DataJpaTest — Repository Layer Only

@DataJpaTest
class ProductRepositoryTest {

    @Autowired
    private ProductRepository productRepository;

    @Test
    void shouldFindProductsByCategory() {
        productRepository.save(new Product("Widget", "electronics"));
        productRepository.save(new Product("Gadget", "electronics"));
        productRepository.save(new Product("Book", "media"));

        List<Product> electronics = productRepository.findByCategory("electronics");

        assertThat(electronics).hasSize(2);
    }
}

Loads only JPA-related beans. Uses an embedded H2 database by default. Fast and isolated.

@RestClientTest — HTTP Client Testing

@RestClientTest(PaymentClient.class)
class PaymentClientTest {

    @Autowired
    private PaymentClient paymentClient;

    @Autowired
    private MockRestServiceServer mockServer;

    @Test
    void shouldCallPaymentGateway() {
        mockServer.expect(requestTo("/payments"))
            .andRespond(withSuccess("{\"status\":\"approved\"}", MediaType.APPLICATION_JSON));

        PaymentResult result = paymentClient.processPayment(new PaymentRequest(...));

        assertThat(result.getStatus()).isEqualTo("approved");
    }
}

Context Caching: The Most Misunderstood Feature

Spring caches the ApplicationContext between test classes. If two test classes have the same configuration (same annotations, same properties, same profiles), they share one context instance. This is why your test suite doesn't restart Spring for every single test class.

What busts the cache:

  • Using @MockBean or @SpyBean (each unique combination creates a new context)
  • Using @DirtiesContext (forces context reload after the test)
  • Different @ActiveProfiles
  • Different properties in @SpringBootTest
// BAD: every test class that uses @MockBean for different beans gets its own context
@SpringBootTest
class OrderServiceTest {
    @MockBean PaymentGateway paymentGateway; // unique context
}

@SpringBootTest  
class InventoryServiceTest {
    @MockBean ShippingService shippingService; // another unique context
}

Better approach: Consolidate mocks into a shared base class or test configuration:

@TestConfiguration
public class SharedMockConfiguration {
    @Bean
    @Primary
    public PaymentGateway mockPaymentGateway() {
        return Mockito.mock(PaymentGateway.class);
    }

    @Bean
    @Primary
    public ShippingService mockShippingService() {
        return Mockito.mock(ShippingService.class);
    }
}

@SpringBootTest
@Import(SharedMockConfiguration.class)
class OrderServiceTest {
    // shares context with any other test using @Import(SharedMockConfiguration.class)
}

Using Profiles in Integration Tests

A test profile lets you override production configuration without @MockBean:

# src/test/resources/application-test.yaml
spring:
  datasource:
    url: jdbc:h2:mem:testdb
  mail:
    host: localhost
    port: 3025  # fake SMTP server

external-payment-api:
  url: http://localhost:8089  # WireMock
@SpringBootTest
@ActiveProfiles("test")
class OrderFlowIntegrationTest {
    // Uses H2 instead of PostgreSQL, WireMock instead of real payment API
}

Overriding Properties Inline

For one-off property overrides without a full profile:

@SpringBootTest(properties = {
    "feature.new-checkout-flow.enabled=true",
    "spring.datasource.url=jdbc:h2:mem:testdb"
})
class NewCheckoutFlowTest { ... }

Practical Patterns for Large Test Suites

Pattern 1: Annotated base class

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles("test")
@Transactional
public abstract class IntegrationTestBase {

    @Autowired
    protected TestRestTemplate restTemplate;

    @LocalServerPort
    protected int port;

    protected String url(String path) {
        return "http://localhost:" + port + path;
    }
}

class OrderApiTest extends IntegrationTestBase {
    @Test
    void shouldCreateOrder() {
        ResponseEntity<OrderDto> response = restTemplate.postForEntity(
            url("/api/orders"), new CreateOrderRequest(...), OrderDto.class
        );
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
    }
}

Pattern 2: Testing asynchronous behavior

@SpringBootTest
class AsyncOrderProcessorTest {

    @Autowired
    private OrderProcessor orderProcessor;

    @Test
    void shouldProcessOrderAsynchronously() throws Exception {
        CompletableFuture<OrderResult> future = orderProcessor.processAsync(testOrder());

        OrderResult result = future.get(5, TimeUnit.SECONDS);

        assertThat(result.getStatus()).isEqualTo(OrderStatus.COMPLETED);
    }
}

When to Use @SpringBootTest vs Slices

Scenario Use
Testing controller request/response mapping @WebMvcTest
Testing JPA queries and repository logic @DataJpaTest
Testing service logic with real dependencies @SpringBootTest + NONE
Testing full HTTP flow including security @SpringBootTest + RANDOM_PORT
Testing external HTTP client behavior @RestClientTest
Testing Redis/cache behavior @DataRedisTest
Testing Kafka producers/consumers @EmbeddedKafka + @SpringBootTest

Integrating with End-to-End Testing Tools

For end-to-end validation of your Spring Boot application's behavior — verifying that flows work correctly from the user's perspective — tools like HelpMeTest complement your integration test suite. While @SpringBootTest with RANDOM_PORT tests the HTTP layer, end-to-end testing catches UI and flow issues that pure API tests miss.

Common Mistakes

Mistake 1: Using @SpringBootTest for every test

If you only need to test a @Service class, use @SpringBootTest(webEnvironment = NONE) or better yet, just use plain JUnit with Mockito. No Spring context needed.

Mistake 2: Forgetting @Transactional on tests

Without @Transactional, test data persists between test methods and can cause order-dependent failures:

@SpringBootTest
@Transactional  // rolls back after each test method
class OrderServiceTest { ... }

Mistake 3: Not cleaning up between tests

If you can't use @Transactional (e.g., because your service uses REQUIRES_NEW transactions), use @Sql to clean up:

@SpringBootTest
@Sql(scripts = "/clean-db.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
class OrderServiceTest { ... }

Summary

@SpringBootTest is powerful but has a cost. Use it for true integration tests that need the full context. For anything more focused, reach for the appropriate slice annotation. Keep your @MockBean usage consistent across test classes to maximize context cache hits, and use profiles or inline properties to control environment-specific behavior. With these patterns in place, your integration test suite will be both thorough and fast.

Read more

Start now free