Spring Boot Testing: Complete Guide
Spring Boot's testing support is one of its strongest features. Between JUnit 5, Mockito, AssertJ, and a suite of purpose-built test slice annotations, you get a layered testing strategy that lets you write fast unit tests, focused slice tests, and full integration tests — all within the same project.
This guide covers the full picture: what each layer tests, how to configure it, and where the performance trade-offs land.
Testing Layers in Spring Boot
Spring Boot applications typically follow three testing layers:
- Unit tests — test a single class in isolation, no Spring context
- Slice tests — load a minimal Spring context scoped to one layer (web, persistence, etc.)
- Integration tests — load the full application context and test end-to-end behavior
Each layer has a different speed and confidence trade-off. Unit tests run in milliseconds; full integration tests can take seconds. A healthy test suite uses all three.
Test Dependencies: spring-boot-starter-test
Add the test starter to your pom.xml or build.gradle:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>This single dependency pulls in everything you need:
- JUnit 5 (Jupiter) — the test runner
- Mockito — mocking framework
- AssertJ — fluent assertions
- Hamcrest — matcher library
- MockMvc — Spring MVC test support
- JsonPath — JSON response assertions
No manual version management needed — Spring Boot's BOM pins all versions.
Unit Tests: No Spring Context
For pure business logic, skip the Spring context entirely. Use Mockito directly:
class OrderServiceTest {
@Mock
private OrderRepository orderRepository;
@InjectMocks
private OrderService orderService;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void shouldCalculateTotalWithDiscount() {
Order order = new Order(List.of(
new LineItem("widget", 10.00, 3)
));
when(orderRepository.findById(1L)).thenReturn(Optional.of(order));
BigDecimal total = orderService.calculateTotal(1L, 0.10);
assertThat(total).isEqualByComparingTo("27.00");
}
}These tests have zero Spring overhead. Run thousands of them in seconds.
@SpringBootTest: Full Integration Tests
@SpringBootTest loads the entire application context — all beans, configurations, and auto-configurations:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class OrderApiIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
void shouldCreateOrderAndReturnId() {
CreateOrderRequest request = new CreateOrderRequest("customer-1", List.of(
new LineItemRequest("widget", 3)
));
ResponseEntity<OrderResponse> response = restTemplate.postForEntity(
"/api/orders", request, OrderResponse.class
);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(response.getBody().getId()).isNotNull();
}
}WebEnvironment.RANDOM_PORT starts a real embedded server on an available port. Use MOCK when you want MockMvc instead of a live server.
Full context tests are the slowest. Use them for critical paths and cross-cutting concerns — not for every endpoint variation.
@WebMvcTest: Controller Layer Only
@WebMvcTest loads only the web layer: controllers, filters, @ControllerAdvice, and MVC configuration. Service and repository beans are not created — you must mock them:
@WebMvcTest(OrderController.class)
class OrderControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private OrderService orderService;
@Test
void shouldReturn201WhenOrderCreated() throws Exception {
OrderResponse mockResponse = new OrderResponse(42L, "PENDING");
when(orderService.createOrder(any())).thenReturn(mockResponse);
mockMvc.perform(post("/api/orders")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"customerId": "c1", "items": [{"sku": "widget", "qty": 3}]}
"""))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").value(42))
.andExpect(jsonPath("$.status").value("PENDING"));
}
@Test
void shouldReturn400WhenRequestBodyInvalid() throws Exception {
mockMvc.perform(post("/api/orders")
.contentType(MediaType.APPLICATION_JSON)
.content("{}"))
.andExpect(status().isBadRequest());
}
}@WebMvcTest starts much faster than @SpringBootTest because it skips the persistence layer entirely. Use it for testing request mapping, validation, serialization, and error handling.
@DataJpaTest: Repository Layer Only
@DataJpaTest configures an in-memory H2 database, scans @Entity classes, and loads JPA repositories. No web layer, no services:
@DataJpaTest
class OrderRepositoryTest {
@Autowired
private OrderRepository orderRepository;
@Autowired
private TestEntityManager entityManager;
@Test
void shouldFindOrdersByCustomerId() {
entityManager.persist(new Order("customer-1", OrderStatus.PENDING));
entityManager.persist(new Order("customer-1", OrderStatus.SHIPPED));
entityManager.persist(new Order("customer-2", OrderStatus.PENDING));
entityManager.flush();
List<Order> orders = orderRepository.findByCustomerId("customer-1");
assertThat(orders).hasSize(2);
assertThat(orders).allMatch(o -> o.getCustomerId().equals("customer-1"));
}
}By default, each test runs in a transaction that rolls back after the test — your database stays clean between tests without manual teardown.
@MockBean and @SpyBean
When Spring context tests need a real bean replaced with a mock, use @MockBean. It registers a Mockito mock into the application context:
@SpringBootTest
class NotificationTest {
@MockBean
private EmailService emailService; // replaces the real bean in context
@Autowired
private OrderService orderService;
@Test
void shouldSendConfirmationEmailOnOrderCreation() {
orderService.createOrder(validRequest());
verify(emailService).sendOrderConfirmation(
argThat(email -> email.getTo().equals("customer@example.com"))
);
}
}@SpyBean wraps the real bean with a Mockito spy — the real method runs unless you explicitly stub it:
@SpyBean
private AuditService auditService; // real bean, but you can verify calls or stub selectivelyUse @MockBean when you want full control. Use @SpyBean when you want real behavior with the ability to assert calls or override specific methods.
TestRestTemplate and WebTestClient
For HTTP-level tests against a running server, Spring Boot provides two clients.
TestRestTemplate works with @SpringBootTest(webEnvironment = RANDOM_PORT) and handles cookies, follows redirects, and suppresses error-on-4xx:
@Autowired
private TestRestTemplate restTemplate;
@Test
void shouldReturnOrderDetails() {
ResponseEntity<OrderResponse> response = restTemplate.getForEntity(
"/api/orders/1", OrderResponse.class
);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}WebTestClient is the reactive alternative and also works for non-reactive apps in Spring Boot 3:
@Autowired
private WebTestClient webTestClient;
@Test
void shouldReturnOrderList() {
webTestClient.get().uri("/api/orders")
.exchange()
.expectStatus().isOk()
.expectBodyList(OrderResponse.class)
.hasSize(3);
}@TestConfiguration and Test Profiles
For test-specific beans, use @TestConfiguration. It supplements (rather than replaces) the main application configuration:
@TestConfiguration
class TestSecurityConfig {
@Bean
@Primary
public SecurityService securityService() {
return new NoOpSecurityService(); // bypass auth in tests
}
}For test-specific properties, create src/test/resources/application-test.properties:
spring.datasource.url=jdbc:h2:mem:testdb
spring.jpa.hibernate.ddl-auto=create-drop
app.email.enabled=false
app.external-api.base-url=http://localhost:8089Activate with @ActiveProfiles("test") on your test class. You can also use @SpringBootTest(properties = {...}) for inline overrides when a full profile is overkill.
Testcontainers: Real Database Testing
H2 is fast but it's not production. Testcontainers spins up real Docker containers during tests:
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
class OrderRepositoryContainerTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16")
.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 OrderRepository orderRepository;
@Test
void shouldPersistAndRetrieveOrder() {
Order saved = orderRepository.save(new Order("customer-1", OrderStatus.PENDING));
Optional<Order> found = orderRepository.findById(saved.getId());
assertThat(found).isPresent();
assertThat(found.get().getCustomerId()).isEqualTo("customer-1");
}
}@DynamicPropertySource wires the container's dynamic port and credentials into the Spring context before it starts. Use Testcontainers when you need to test database-specific queries, constraints, or migrations that H2 won't replicate accurately.
Test Slices vs Full Context: Performance Considerations
Loading the full Spring context takes time — often 5–15 seconds per test class on a real application. Test slices exist to avoid this penalty:
| Annotation | Context Scope | Startup Cost |
|---|---|---|
@WebMvcTest |
Web layer only | Low |
@DataJpaTest |
JPA layer only | Low |
@WebFluxTest |
WebFlux layer only | Low |
@JsonTest |
JSON serialization only | Very low |
@SpringBootTest |
Full application | High |
Spring Boot caches application contexts across tests. Tests that share the same context configuration will reuse the already-started context — which is why mixing @MockBean with integration tests can hurt performance: each unique @MockBean combination creates a new context.
Best practice: group tests that share the same mock configuration into the same class. Reserve @SpringBootTest for smoke tests and cross-layer integration scenarios. Run slice tests for everything else — they give you 90% of the confidence at a fraction of the startup cost.
A practical split for most projects:
- 70% unit tests (no Spring context)
- 25% slice tests (
@WebMvcTest,@DataJpaTest) - 5% full integration tests (
@SpringBootTest)
This keeps the test suite fast enough to run on every commit without developers skipping it.
Test the Full User Journey
Spring Boot testing covers your backend. For end-to-end browser tests that verify the complete user experience, HelpMeTest generates and runs UI tests automatically with 24/7 monitoring — starting free.