@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:
- Searches for the main
@SpringBootApplicationclass (walking up the package hierarchy) - Loads the full
ApplicationContextwith all beans, auto-configurations, and property sources - 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
@MockBeanor@SpyBean(each unique combination creates a new context) - Using
@DirtiesContext(forces context reload after the test) - Different
@ActiveProfiles - Different
propertiesin@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.