Spring Boot Test Slices: @WebMvcTest, @DataJpaTest & MockMvc in Production
Every time you use @SpringBootTest where a test slice would suffice, you're paying a startup tax you didn't need to. Spring Boot's test slice annotations — @WebMvcTest, @DataJpaTest, @RestClientTest, @DataMongoTest — load only the subsystem relevant to the test, cutting context startup from 15 seconds to under 2. This guide covers the complete test slice catalog: when to use each, what they load, what you need to mock out, and how to combine them with Testcontainers for the repository layer.
Table of Contents
- What Are Test Slices and Why They Matter
- @WebMvcTest: Controller Layer Testing with MockMvc
- MockMvc Patterns: Request Building, JSON Assertions, and Security
- @DataJpaTest: Repository Testing with Real SQL
- Combining @DataJpaTest with Testcontainers
- @RestClientTest: Testing HTTP Clients Without Hitting Real Endpoints
- Other Slices: @DataMongoTest, @DataRedisTest, @JsonTest
- Creating a Custom Test Slice for Your Domain
- Key Takeaways
1. What Are Test Slices and Why They Matter
A test slice is a custom @SpringBootTest configuration that auto-configures only a specific layer of the application. Spring Boot's auto-configuration excludes all beans not relevant to the slice, creating a lean context. The trade-off: you must mock all dependencies outside the slice boundary using @MockBean.
| Annotation | What It Loads | What You Mock | Startup Time |
|---|---|---|---|
@WebMvcTest |
Controllers, filters, Jackson, security | Services, repositories | 1–3 s |
@DataJpaTest |
JPA, Hibernate, DataSource, Flyway/Liquibase | Services, HTTP clients | 2–5 s |
@RestClientTest |
RestTemplate, WebClient, Jackson, MockRestServiceServer | Services that use the client | 1–2 s |
@DataMongoTest |
MongoDB repositories, embedded Mongo | Services | 2–4 s |
@JsonTest |
Jackson ObjectMapper, JsonTester | Everything else | <1 s |
2. @WebMvcTest: Controller Layer Testing with MockMvc
@WebMvcTest instantiates the Spring MVC infrastructure — DispatcherServlet, handler mappings, message converters, filters — but not the service layer or database. It automatically wires a MockMvc bean for sending simulated HTTP requests without a running server. Specify the controller under test explicitly: @WebMvcTest(OrderController.class). Without a class name, all controllers are loaded, negating the speed benefit.
@WebMvcTest(OrderController.class)
class OrderControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private OrderService orderService; // mocked — outside the slice
@Test
void shouldReturn201WhenOrderCreatedSuccessfully() throws Exception {
OrderRequest request = new OrderRequest("user-1", "sku-A", 2);
OrderResponse response = new OrderResponse(UUID.randomUUID(), "PENDING");
when(orderService.placeOrder(any(OrderRequest.class))).thenReturn(response);
mockMvc.perform(post("/api/orders")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.status").value("PENDING"))
.andExpect(jsonPath("$.orderId").isNotEmpty())
.andExpect(header().exists("Location"));
}
@Test
void shouldReturn400WhenRequestBodyIsInvalid() throws Exception {
OrderRequest invalidRequest = new OrderRequest(null, "sku-A", -1);
mockMvc.perform(post("/api/orders")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(invalidRequest)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errors.userId").value("userId is required"))
.andExpect(jsonPath("$.errors.quantity").value("quantity must be positive"));
}
@Test
void shouldReturn404WhenOrderNotFound() throws Exception {
UUID unknownId = UUID.randomUUID();
when(orderService.getOrder(unknownId))
.thenThrow(new OrderNotFoundException(unknownId));
mockMvc.perform(get("/api/orders/{id}", unknownId))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.message").value(containsString(unknownId.toString())));
}
}3. MockMvc Patterns: Request Building, JSON Assertions, and Security
MockMvc includes the full filter chain, including Spring Security. Use @WithMockUser from spring-security-test to inject a mock principal, or build a JWT token using a test helper. The andDo(print()) method dumps the full request/response to the console for debugging.
// Security testing with @WithMockUser
@Test
@WithMockUser(roles = "ADMIN")
void shouldReturn200ForAdminAccessToOrderList() throws Exception {
when(orderService.findAll()).thenReturn(List.of());
mockMvc.perform(get("/api/admin/orders"))
.andExpect(status().isOk());
}
@Test
void shouldReturn403ForUnauthenticatedRequest() throws Exception {
mockMvc.perform(get("/api/orders/mine"))
.andExpect(status().isUnauthorized());
}
// JWT bearer token approach
@Test
void shouldReturn200WithValidJwt() throws Exception {
String jwt = jwtHelper.createToken("user-1", List.of("ROLE_USER"));
mockMvc.perform(get("/api/orders/mine")
.header("Authorization", "Bearer " + jwt))
.andExpect(status().isOk());
}
// ResultActions chaining for complex assertions
mockMvc.perform(get("/api/orders").param("page", "0").param("size", "10"))
.andDo(print()) // debug output
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.content", hasSize(lessThanOrEqualTo(10))))
.andExpect(jsonPath("$.totalElements").isNumber())
.andExpect(jsonPath("$.content[0].orderId").isNotEmpty());
// Asserting response headers
mockMvc.perform(post("/api/orders").contentType(APPLICATION_JSON).content("{}"))
.andExpect(header().string("Content-Type", containsString("application/json")))
.andExpect(header().string("Location", matchesPattern("/api/orders/[\\w-]+")));4. @DataJpaTest: Repository Testing with Real SQL
@DataJpaTest loads the JPA stack — entity manager, repositories, Hibernate — and by default replaces your configured data source with an in-memory H2. Each test runs in a transaction that is rolled back after, keeping tests isolated. For real SQL dialects, override the data source with Testcontainers (covered in the next section).
@DataJpaTest
class OrderRepositoryTest {
@Autowired
private OrderRepository orderRepository;
@Autowired
private TestEntityManager entityManager;
@Test
void shouldFindOrdersByCustomerEmailAndStatus() {
// Use TestEntityManager for setup — bypasses the repository under test
entityManager.persistAndFlush(new Order("alice@example.com", "PENDING", null));
entityManager.persistAndFlush(new Order("alice@example.com", "SHIPPED", null));
entityManager.persistAndFlush(new Order("bob@example.com", "PENDING", null));
List<Order> alicePending = orderRepository
.findByCustomerEmailAndStatus("alice@example.com", "PENDING");
assertThat(alicePending).hasSize(1);
assertThat(alicePending.get(0).getCustomerEmail()).isEqualTo("alice@example.com");
}
@Test
void shouldReturnTopNMostRecentOrders() {
LocalDateTime now = LocalDateTime.now();
for (int i = 0; i < 15; i++) {
entityManager.persist(
new Order("user@example.com", "COMPLETED", now.minusDays(i))
);
}
entityManager.flush();
List<Order> top5 = orderRepository.findTop5ByCustomerEmailOrderByCreatedAtDesc(
"user@example.com"
);
assertThat(top5).hasSize(5);
assertThat(top5.get(0).getCreatedAt()).isAfterOrEqualTo(top5.get(1).getCreatedAt());
}
@Test
void shouldUpdateOrderStatusAndReturnUpdatedCount() {
UUID orderId = entityManager.persistAndGetId(
new Order("user@example.com", "PENDING", null),
UUID.class
);
entityManager.flush();
int updated = orderRepository.updateStatus(orderId, "SHIPPED");
assertThat(updated).isEqualTo(1);
Order updated_order = entityManager.find(Order.class, orderId);
assertThat(updated_order.getStatus()).isEqualTo("SHIPPED");
}
}5. Combining @DataJpaTest with Testcontainers
Replace the H2 data source with a real PostgreSQL container to test dialect-specific features: jsonb queries, pg_trgm similarity, unnest, and generate_series. Add @AutoConfigureTestDatabase(replace = NONE) to disable H2 auto-configuration, then declare the container with @ServiceConnection.
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
class ProductRepositoryPostgresTest {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:16-alpine")
.withInitScript("db/init-extensions.sql"); // enable pg_trgm
@Autowired
private ProductRepository productRepository;
@Test
void shouldSearchProductsByNameUsingSimilarity() {
productRepository.saveAll(List.of(
new Product("MacBook Pro 14", "ELECTRONICS"),
new Product("MacBook Air 15", "ELECTRONICS"),
new Product("iPad Pro 12.9", "ELECTRONICS")
));
// Uses pg_trgm similarity index — would fail on H2
List<Product> results = productRepository.findBySimilarName("Macbook");
assertThat(results).hasSize(2);
assertThat(results).extracting(Product::getName)
.allMatch(name -> name.toLowerCase().contains("macbook"));
}
@Test
void shouldQueryJsonbMetadataField() {
Product p = new Product("Laptop", "ELECTRONICS");
p.setMetadata("{\"brand\": \"Dell\", \"ram\": \"16GB\"}");
productRepository.save(p);
// jsonb operator ->> — PostgreSQL specific, fails on H2
List<Product> dellProducts = productRepository.findByBrand("Dell");
assertThat(dellProducts).hasSize(1);
}
}6. @RestClientTest: Testing HTTP Clients Without Hitting Real Endpoints
@RestClientTest loads only the HTTP client infrastructure and Jackson. It wires a MockRestServiceServer that intercepts outgoing HTTP requests and returns pre-configured responses — no network, no real endpoints. Use it to test your RestTemplate or RestClient wrapper classes in isolation.
@RestClientTest(InventoryClient.class)
class InventoryClientTest {
@Autowired
private InventoryClient inventoryClient;
@Autowired
private MockRestServiceServer mockServer;
@Autowired
private ObjectMapper objectMapper;
@Test
void shouldReturnTrueWhenInventoryIsAvailable() throws Exception {
InventoryResponse response = new InventoryResponse("sku-A", true, 50);
mockServer.expect(requestTo("/api/inventory/sku-A"))
.andExpect(method(HttpMethod.GET))
.andExpect(header("Accept", MediaType.APPLICATION_JSON_VALUE))
.andRespond(withSuccess(
objectMapper.writeValueAsString(response),
MediaType.APPLICATION_JSON
));
boolean available = inventoryClient.isAvailable("sku-A", 10);
assertThat(available).isTrue();
mockServer.verify();
}
@Test
void shouldThrowInventoryServiceExceptionOnServerError() {
mockServer.expect(requestTo("/api/inventory/sku-B"))
.andRespond(withServerError());
assertThatThrownBy(() -> inventoryClient.isAvailable("sku-B", 1))
.isInstanceOf(InventoryServiceException.class)
.hasMessageContaining("Inventory service unavailable");
}
@Test
void shouldHandleTimeoutGracefully() {
mockServer.expect(requestTo("/api/inventory/sku-C"))
.andRespond(withException(new SocketTimeoutException("Read timed out")));
assertThatThrownBy(() -> inventoryClient.isAvailable("sku-C", 1))
.isInstanceOf(InventoryServiceException.class)
.hasMessageContaining("timeout");
}
}7. Other Slices: @DataMongoTest, @DataRedisTest, @JsonTest
Spring Boot ships more than a dozen slice annotations. The most useful beyond the core three are:
// @JsonTest — test serialization/deserialization without a full context
@JsonTest
class OrderResponseJsonTest {
@Autowired
private JacksonTester<OrderResponse> json;
@Test
void shouldSerializeOrderResponseToJson() throws Exception {
OrderResponse response = new OrderResponse(
UUID.fromString("a1b2c3d4-e5f6-7890-abcd-ef1234567890"),
"PENDING",
Instant.parse("2026-04-04T10:00:00Z")
);
JsonContent<OrderResponse> written = json.write(response);
assertThat(written).extractingJsonPathStringValue("$.status").isEqualTo("PENDING");
assertThat(written).extractingJsonPathStringValue("$.orderId")
.isEqualTo("a1b2c3d4-e5f6-7890-abcd-ef1234567890");
assertThat(written).extractingJsonPathStringValue("$.createdAt")
.isEqualTo("2026-04-04T10:00:00Z");
}
@Test
void shouldDeserializeJsonToOrderResponse() throws Exception {
String json_str = "{\"orderId\":\"a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\"status\":\"SHIPPED\"}";
OrderResponse response = json.parseObject(json_str);
assertThat(response.getStatus()).isEqualTo("SHIPPED");
}
}
// @DataMongoTest — MongoDB repositories with embedded Mongo
@DataMongoTest
class AuditLogRepositoryTest {
@Autowired
private AuditLogRepository auditLogRepository;
@Test
void shouldFindLogsWithinDateRange() {
auditLogRepository.insert(new AuditLog("user-1", "LOGIN", Instant.now().minusDays(1)));
auditLogRepository.insert(new AuditLog("user-1", "LOGOUT", Instant.now()));
List<AuditLog> logs = auditLogRepository.findByUserIdAndTimestampBetween(
"user-1",
Instant.now().minus(2, ChronoUnit.DAYS),
Instant.now().plus(1, ChronoUnit.HOURS)
);
assertThat(logs).hasSize(2);
}
}8. Creating a Custom Test Slice for Your Domain
If your application has a custom layer — a messaging layer with Kafka producers and serializers, or a caching layer with custom Redis configuration — you can create a domain-specific test slice using @TypeExcludeFilters and a custom TypeExcludeFilter. This is an advanced technique that Spring uses internally for all its built-in slices.
// Custom slice annotation
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(DataJpaTestContextBootstrapper.class)
@ExtendWith(SpringExtension.class)
@OverrideAutoConfiguration(enabled = false)
@TypeExcludeFilters(KafkaTypeExcludeFilter.class)
@AutoConfigureCache
@AutoConfigureKafka // custom auto-configuration
@ImportAutoConfiguration // list of auto-configs to include
public @interface KafkaProducerTest {
// custom attributes if needed
}
// Usage
@KafkaProducerTest
class OrderEventProducerTest {
@Autowired
private KafkaTemplate<String, OrderEvent> kafkaTemplate;
@Autowired
private OrderEventProducer producer;
// No Kafka broker needed — tests serialization and header logic only
}9. Key Takeaways
- Always specify the controller class in
@WebMvcTest(MyController.class)— without it all controllers load, eliminating the speed benefit. - Use
@DataJpaTest+@AutoConfigureTestDatabase(replace = NONE)+ Testcontainers for PostgreSQL-specific query testing. - Use
@RestClientTest+MockRestServiceServerto test HTTP client wrappers — including error handling and timeouts — without a real server. - Use
@JsonTestto test Jackson serialization rules, custom serializers, and@JsonPropertymappings in isolation. - Each slice auto-configures its layer and disables everything else — you must
@MockBeanall out-of-slice dependencies. - Spring Boot's slice annotations share the application context within a test class — across test methods, the context is reused, not restarted.
- Prefer
TestEntityManagerover calling the repository under test in@DataJpaTestsetup — it bypasses the query you are testing. - Slice tests run in a transaction by default and roll back after each test — no cleanup needed for pure database assertions.
10. Testing Security with @WebMvcTest: @WithMockUser and Custom SecurityContext
@WebMvcTest loads the Spring Security filter chain alongside controllers, which means your security configuration is active during slice tests. This is exactly what you want — it lets you verify that unauthenticated requests are rejected, that roles are enforced correctly, and that the controller returns the right HTTP status codes for different principals. The Spring Security Test module provides @WithMockUser and @WithMockJwt to inject a fake SecurityContext without a real authentication server.
@WebMvcTest(OrderController.class)
class OrderControllerSecurityTest {
@Autowired MockMvc mockMvc;
@MockBean OrderService orderService;
@Test
void getOrder_unauthenticated_returns401() throws Exception {
mockMvc.perform(get("/api/orders/42"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(roles = "CUSTOMER")
void getOrder_asCustomer_returns200() throws Exception {
given(orderService.findById(42L)).willReturn(Optional.of(sampleOrder()));
mockMvc.perform(get("/api/orders/42"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(42));
}
@Test
@WithMockUser(roles = "ADMIN")
void deleteOrder_asAdmin_returns204() throws Exception {
mockMvc.perform(delete("/api/orders/42"))
.andExpect(status().isNoContent());
}
@Test
@WithMockUser(roles = "CUSTOMER")
void deleteOrder_asCustomer_returns403() throws Exception {
mockMvc.perform(delete("/api/orders/42"))
.andExpect(status().isForbidden());
}
}
For JWT-secured APIs, the @WithMockUser approach populates a UsernamePasswordAuthenticationToken, which does not match the JwtAuthenticationToken type your method-level security expressions may check. In those cases, create a custom SecurityMockMvcRequestPostProcessor or use the jwt() request post-processor from spring-security-test to inject a properly typed mock JWT with the required claims.
// Injecting a mock JWT with specific claims using SecurityMockMvcRequestPostProcessors
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
@Test
void getOrder_withJwtClaims_returnsOrderForOwner() throws Exception {
mockMvc.perform(get("/api/orders/42")
.with(jwt()
.jwt(jwt -> jwt
.subject("user-uuid-123")
.claim("roles", List.of("ROLE_CUSTOMER"))
.claim("tenant_id", "acme-corp"))))
.andExpect(status().isOk());
}
// Testing CSRF protection on state-changing endpoints
@Test
void createOrder_withoutCsrf_returns403() throws Exception {
mockMvc.perform(post("/api/orders")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"productId\": 1, \"quantity\": 2}"))
// No .with(csrf()) — CSRF token absent
.andExpect(status().isForbidden());
}
A common mistake is disabling security in @WebMvcTest by excluding SecurityAutoConfiguration. While this makes tests simpler, it defeats the purpose of slice testing — you lose verification that security is applied correctly. The right approach is to keep security enabled, use @MockBean for any authentication infrastructure beans (UserDetailsService, JwtDecoder) that the filter chain requires, and write explicit tests for both authenticated and unauthenticated scenarios. This ensures that a security misconfiguration (e.g., accidentally permitting an admin endpoint) is caught at the test layer.
For method-level security annotations like @PreAuthorize("hasRole('ADMIN') or #userId == principal.username"), @WebMvcTest executes those expressions against the mock security context. This allows you to test both the happy path (user has the right role or owns the resource) and the authorization failure path without ever touching the actual role hierarchy configuration. When combined with @EnableMethodSecurity in a test configuration, slice tests can fully validate complex security policies at the controller boundary.
11. Performance Testing Slices: Detecting N+1 Queries in @DataJpaTest
One of the most valuable — and underused — capabilities of @DataJpaTest is query count verification. The N+1 query problem is notoriously hard to detect in production because individual queries are fast, but the aggregate effect of 100 queries per request instead of 1 degrades performance significantly under load. By intercepting Hibernate's SQL output in a slice test, you can assert the exact number of queries executed for a given repository operation and fail the build when a developer accidentally introduces an N+1.
<!-- Add Datasource Proxy for SQL interception -->
<dependency>
<groupId>net.ttddyy</groupId>
<artifactId>datasource-proxy</artifactId>
<version>1.10</version>
<scope>test</scope>
</dependency>@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
class OrderRepositoryQueryCountTest {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
@Autowired TestEntityManager em;
@Autowired OrderRepository orderRepository;
// Datasource proxy — counts SELECT statements
@Autowired DataSource dataSource;
private ProxyDataSourceBuilder proxyBuilder;
@Test
void findAllWithItems_shouldIssueExactlyOneQuery() {
// Given: 5 orders, each with 3 items
for (int i = 0; i < 5; i++) {
Order order = new Order("customer-" + i);
order.addItem(new OrderItem("SKU-A", 1));
order.addItem(new OrderItem("SKU-B", 2));
order.addItem(new OrderItem("SKU-C", 3));
em.persistAndFlush(order);
}
em.clear(); // Clear persistence context to force real DB queries
// Wrap datasource in proxy to count queries
QueryCountHolder.clearCount();
List<Order> orders = orderRepository.findAllWithItemsFetchJoin();
// Assert: fetch join should issue exactly 1 query, not 1 + 5 (N+1)
QueryCount queryCount = QueryCountHolder.getGrandTotal();
assertThat(queryCount.getSelect())
.as("Expected 1 SQL SELECT (fetch join), but got N+1")
.isEqualTo(1);
assertThat(orders).hasSize(5);
assertThat(orders.get(0).getItems()).hasSize(3);
}
}
Alternatively, configure Hibernate's statistics and use SessionFactory.getStatistics().getPrepareStatementCount() for query counting without an additional library. Enable Hibernate stats in test configuration with spring.jpa.properties.hibernate.generate_statistics=true. While less granular than DataSource Proxy, Hibernate statistics are available out of the box and sufficient for detecting obvious N+1 patterns.
Beyond query counting, @DataJpaTest is an excellent place to verify database constraint enforcement. Attempt to persist an entity that violates a unique constraint, a foreign key constraint, or a not-null column, and assert that the expected exception is thrown. This is far more reliable than checking application-level validation logic because it tests the actual database schema — including any migrations applied by Flyway or Liquibase — rather than just the Java validation annotations. Constraint violations discovered at the repository layer reveal mismatches between the entity model and the schema before they become production incidents.
12. Integration Testing Strategy: When to Use Which Slice
Choosing the right test type is as important as writing the test. A full @SpringBootTest that takes 30 seconds to start should not be the default — it is a last resort for tests that genuinely need the complete application context. Slice tests are fast, focused, and catch layer-specific bugs without the overhead of booting everything. The decision tree below guides the choice.
| What You Are Testing | Recommended Slice | Mock Out |
|---|---|---|
| Controller routing, request validation, response serialization, security | @WebMvcTest | Services, repositories, external clients |
| JPA repository queries, entity mapping, database constraints, N+1 | @DataJpaTest + Testcontainers | Services, HTTP clients, Kafka |
| HTTP client wrapper (RestTemplate/WebClient), retry, timeout, error mapping | @RestClientTest | Everything except the HTTP client bean under test |
| JSON serialization/deserialization rules, custom serializers, naming strategy | @JsonTest | Everything — only Jackson is loaded |
| MongoDB repository queries, aggregation pipelines | @DataMongoTest + Testcontainers | Services, HTTP clients |
| Full request-to-database flow spanning controller + service + repository | @SpringBootTest + Testcontainers | External HTTP APIs (use WireMock) |
A productive test strategy uses slices for the majority of tests and reserves @SpringBootTest for a small number of end-to-end "smoke" tests that verify critical user journeys. A reasonable distribution for a medium-sized microservice might be: 200 unit tests (no Spring context), 30 @WebMvcTest tests, 20 @DataJpaTest tests, 10 @RestClientTest tests, and 5 @SpringBootTest end-to-end tests. This distribution keeps total CI test time under 5 minutes while providing excellent coverage of all application layers.
Application context caching is a critical performance factor when using multiple slice types. Spring Boot caches test application contexts by their configuration key — the combination of annotations, test properties, and mock beans. Tests that share the same context key reuse the cached context without a restart. Avoid creating unique test configurations unnecessarily (e.g., adding @TestPropertySource with per-test values) because this busts the cache and forces a new context startup for each unique configuration, counteracting the speed benefit of slices.
13. Spring Boot 3.2+ Test Improvements: MockMvcTester and AssertJ Integration
Spring Boot 3.2 introduced MockMvcTester, a fluent, AssertJ-native API for MockMvc that eliminates the checked exception ceremony and makes test assertions far more readable. Previously, every mockMvc.perform() call required throws Exception on the test method and chained .andExpect() calls that mix result matchers from different packages. MockMvcTester returns an AssertableMvcResult that integrates directly with AssertJ's rich assertion vocabulary.
// Spring Boot 3.2+: MockMvcTester — no more throws Exception or andExpect chains
@WebMvcTest(OrderController.class)
class OrderControllerTest {
@Autowired MockMvcTester mockMvc; // Injected directly like MockMvc
@MockBean OrderService orderService;
@Test
@WithMockUser(roles = "CUSTOMER")
void getOrder_found_returns200WithBody() {
given(orderService.findById(42L)).willReturn(Optional.of(sampleOrder()));
assertThat(mockMvc.get().uri("/api/orders/{id}", 42))
.hasStatusOk()
.hasContentTypeCompatibleWith(MediaType.APPLICATION_JSON)
.bodyJson()
.extractingPath("$.id").isEqualTo(42)
.extractingPath("$.status").isEqualTo("PENDING");
}
@Test
@WithMockUser(roles = "CUSTOMER")
void createOrder_invalidBody_returns400WithValidationErrors() {
assertThat(mockMvc.post().uri("/api/orders")
.contentType(MediaType.APPLICATION_JSON)
.content("{}")) // Missing required fields
.hasStatus(HttpStatus.BAD_REQUEST)
.bodyJson()
.extractingPath("$.errors").asArray().isNotEmpty();
}
}
Spring Boot 3.2 also introduced auto-configured RestClient and WebClient test support. When your code uses the new RestClient (the fluent replacement for RestTemplate), @RestClientTest now auto-configures a MockRestServiceServer that intercepts RestClient requests in addition to RestTemplate requests. This means you do not need to manually wire up request interceptors for test-time stubbing.
Spring Boot 3.3 added @AutoConfigureHttpExchange, which simplifies testing of Spring HTTP Interface clients — the declarative HTTP clients defined with @HttpExchange, @GetExchange, and similar annotations. Previously, testing these required setting up a full WireMock server or a real server. With the new auto-configuration, a MockServer is automatically wired to the HTTP interface proxy, enabling compact unit-style tests for declarative HTTP clients.
// Testing a Spring HTTP Interface client (Spring Boot 3.2+)
@RestClientTest(InventoryClient.class)
class InventoryClientTest {
@Autowired InventoryClient inventoryClient;
@Autowired MockRestServiceServer server;
@Test
void getStock_found_returnsMappedResponse() {
server.expect(requestTo("/inventory/SKU-A"))
.andExpect(method(HttpMethod.GET))
.andRespond(withSuccess("""
{"sku": "SKU-A", "quantity": 42}
""", MediaType.APPLICATION_JSON));
StockLevel stock = inventoryClient.getStock("SKU-A");
assertThat(stock.quantity()).isEqualTo(42);
server.verify();
}
}
The trend across Spring Boot 3.x test improvements is consistent: reduce boilerplate while keeping tests fast, focused, and expressive. Teams upgrading from Spring Boot 2.x will find that migrating existing MockMvc tests to MockMvcTester is straightforward and immediately produces more readable test code. The AssertJ integration in particular enables better failure messages — when a test fails, the error message describes exactly which JSON field had the wrong value rather than a raw MockMvcResultHandlers output that requires manual parsing to understand.
Spring Boot 3.4 further extended test slice capabilities with improved support for @DataCassandraTest, @DataLdapTest, and @DataR2dbcTest slices for non-relational and reactive data stores. The @DataR2dbcTest slice is particularly notable for teams adopting reactive SQL access — it configures only the R2DBC connection factory, entity callbacks, and R2DBC repositories without loading the full application context, enabling focused repository tests at the reactive layer. Combined with Testcontainers R2DBC support, you can run reactive repository tests against a real PostgreSQL instance with the same ease as traditional JPA slice tests.
Looking ahead, the test slice model continues to be the recommended approach for Spring Boot test architecture. The core principle — test each layer in isolation with the fastest possible context, reserve full-context tests for integration verification — remains constant. As Spring Boot adds support for new infrastructure types, new slices follow automatically. By investing in a slice-first test strategy today, your team builds a fast, maintainable, and resilient test suite that scales with the application without requiring periodic rewrites as the technology stack evolves.
Leave a Comment
Related Posts
Software Engineer · Java · Spring Boot · Microservices