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.
Leave a Comment
Related Posts
Software Engineer · Java · Spring Boot · Microservices