Spring Boot Testing Slices — @DataJpaTest, @WebMvcTest, and @SpringBootTest
@SpringBootTest starts the full application context — all beans, all auto-configurations, all datasources, all message listeners. That is 10–30 seconds of startup time for every test class that needs it. Spring Boot’s test slices load a subset of the application context — only the beans relevant to what you are testing. Repository tests that once took 15 seconds with @SpringBootTest take 2 seconds with @DataJpaTest. This article covers all the major test slices and how to combine them with Testcontainers.
What You’ll Learn
- How test slices work — which beans each slice loads
@DataJpaTestfor JPA repositories@WebMvcTestfor Spring MVC controllers@JsonTestfor JSON serialization/deserialization@DataMongoTestfor MongoDB repositories@SpringBootTest— when you actually need the full context- How to add extra beans to a slice with
@Import
How Test Slices Work
Spring Boot test slices are meta-annotations that:
- Start only a specific subset of the Spring application context
- Auto-configure only the relevant infrastructure
- Scan only relevant component types (repositories, controllers, etc.)
Full @SpringBootTest context:
- All @Component, @Service, @Repository, @Controller
- All auto-configurations
- All datasources, message brokers, etc.
- Startup: 10–30s
@DataJpaTest slice:
- @Repository classes only
- JPA auto-configuration (EntityManager, JpaRepositories)
- DataSource (replaced with H2 unless overridden)
- Startup: 2–5s
@WebMvcTest slice:
- @Controller, @ControllerAdvice, @JsonComponent
- Spring MVC auto-configuration
- MockMvc, Jackson, security filters
- No service, repository, or datasource beans
- Startup: 1–3s
@DataJpaTest
@DataJpaTest loads the JPA layer: EntityManager, Spring Data JPA repositories, datasource (H2 by default), Hibernate, and Flyway/Liquibase if present.
With Testcontainers (the right way)
@DataJpaTest
@Testcontainers
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class OrderRepositoryTest {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:16-alpine");
@Autowired
private OrderRepository orderRepository;
@Autowired
private TestEntityManager entityManager;
@Test
void shouldFindOrdersByCustomer() {
// TestEntityManager bypasses Spring Data to insert directly
entityManager.persist(new Order("customer-1", OrderStatus.PENDING, BigDecimal.valueOf(99.99)));
entityManager.persist(new Order("customer-1", OrderStatus.CONFIRMED, BigDecimal.valueOf(49.99)));
entityManager.persist(new Order("customer-2", OrderStatus.PENDING, BigDecimal.valueOf(79.99)));
entityManager.flush();
List<Order> orders = orderRepository.findByCustomerId("customer-1");
assertThat(orders).hasSize(2);
}
@Test
@Transactional
void shouldRollbackAfterTest() {
// @DataJpaTest wraps each test in a transaction that rolls back.
// @Transactional here makes that explicit.
orderRepository.save(new Order("customer-1", OrderStatus.PENDING, BigDecimal.valueOf(50.00)));
assertThat(orderRepository.count()).isEqualTo(1);
// Rolls back after this test — next test sees an empty table
}
}
@AutoConfigureTestDatabase(replace = NONE) is critical — it prevents Spring Boot from replacing your PostgreSQL datasource with H2.
@DataJpaTest wraps each test in a transaction that rolls back after the test. This provides test isolation without @BeforeEach deleteAll(). However, rollback does not commit, so any behavior that depends on committed data (like AFTER INSERT triggers or cross-transaction behavior) requires a different approach.
Adding Non-JPA Beans to @DataJpaTest
@DataJpaTest does not load @Service beans. If your repository test needs a service dependency, import it explicitly:
@DataJpaTest
@Testcontainers
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import({PasswordEncoder.class, AuditingConfig.class}) // import extra beans
class UserRepositoryTest {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
}
@WebMvcTest
@WebMvcTest loads Spring MVC controllers, ControllerAdvice, JsonComponent, security filters, and MockMvc. It does not load services or repositories — those must be mocked.
@WebMvcTest(OrderController.class)
class OrderControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean // creates a Mockito mock and registers it as a Spring bean
private OrderService orderService;
@MockBean
private OrderEventPublisher eventPublisher;
@Test
void shouldReturnOrderById() throws Exception {
Order order = new Order("customer-1", OrderStatus.PENDING, BigDecimal.valueOf(99.99));
order.setId(1L);
when(orderService.findById(1L)).thenReturn(order);
mockMvc.perform(get("/api/v1/orders/1")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.customerId").value("customer-1"))
.andExpect(jsonPath("$.status").value("PENDING"));
}
@Test
void shouldReturn404WhenOrderNotFound() throws Exception {
when(orderService.findById(99L))
.thenThrow(new OrderNotFoundException(99L));
mockMvc.perform(get("/api/v1/orders/99"))
.andExpect(status().isNotFound());
}
@Test
void shouldValidateCreateOrderRequest() throws Exception {
String invalidRequest = """
{
"customerId": "",
"items": []
}
""";
mockMvc.perform(post("/api/v1/orders")
.contentType(MediaType.APPLICATION_JSON)
.content(invalidRequest))
.andExpect(status().isBadRequest());
}
@Test
void shouldReturn201WithLocationHeaderOnCreate() throws Exception {
CreateOrderRequest request = new CreateOrderRequest(
"customer-1",
List.of(new OrderItem("product-1", 1, BigDecimal.valueOf(99.99)))
);
Order created = new Order("customer-1", OrderStatus.PENDING, BigDecimal.valueOf(99.99));
created.setId(42L);
when(orderService.createOrder(any())).thenReturn(created);
mockMvc.perform(post("/api/v1/orders")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(header().string("Location", containsString("/api/v1/orders/42")));
}
}
@WebMvcTest with Security
@WebMvcTest includes security filters. If your controller requires authentication, configure the test:
@WebMvcTest(OrderController.class)
@Import(SecurityTestConfig.class) // import test security configuration
class SecureOrderControllerTest {
@Test
@WithMockUser(roles = "customer")
void shouldReturnOrdersForAuthenticatedUser() throws Exception {
when(orderService.findAll()).thenReturn(List.of());
mockMvc.perform(get("/api/v1/orders"))
.andExpect(status().isOk());
}
@Test
void shouldReturn401ForUnauthenticatedRequest() throws Exception {
mockMvc.perform(get("/api/v1/orders"))
.andExpect(status().isUnauthorized());
}
@Test
@WithMockUser(roles = "admin")
void shouldAllowAdminToAccessAdminEndpoint() throws Exception {
mockMvc.perform(get("/api/v1/admin/orders"))
.andExpect(status().isOk());
}
}
@JsonTest
@JsonTest tests JSON serialization and deserialization in isolation. It loads JacksonObjectMapperAutoConfiguration and provides a JacksonTester for assertions.
@JsonTest
class OrderResponseJsonTest {
@Autowired
private JacksonTester<OrderResponse> json;
@Test
void shouldSerializeOrderResponse() throws Exception {
OrderResponse response = new OrderResponse(
1L,
"customer-1",
"PENDING",
BigDecimal.valueOf(99.99),
LocalDateTime.of(2026, 5, 1, 10, 0)
);
JsonContent<OrderResponse> result = json.write(response);
assertThat(result).hasJsonPathNumberValue("$.id").isEqualTo(1);
assertThat(result).hasJsonPathStringValue("$.customerId").isEqualTo("customer-1");
assertThat(result).hasJsonPathStringValue("$.status").isEqualTo("PENDING");
assertThat(result).hasJsonPathStringValue("$.createdAt");
// Verify null fields are excluded (Jackson @JsonInclude(NON_NULL))
assertThat(result).doesNotHaveJsonPath("$.internalField");
}
@Test
void shouldDeserializeOrderRequest() throws Exception {
String json = """
{
"customerId": "customer-1",
"items": [
{"productId": "p1", "quantity": 2, "unitPrice": 49.99}
]
}
""";
CreateOrderRequest request = this.json.parse(json).getObject(); // requires separate tester
assertThat(request.customerId()).isEqualTo("customer-1");
assertThat(request.items()).hasSize(1);
}
}
@DataMongoTest
@DataMongoTest is the MongoDB equivalent of @DataJpaTest:
@DataMongoTest
@Testcontainers
class OrderDocumentRepositoryTest {
@Container
@ServiceConnection
static MongoDBContainer mongodb = new MongoDBContainer("mongo:7.0");
@Autowired
private OrderDocumentRepository orderRepository;
@Test
void shouldSaveAndFindDocument() {
OrderDocument order = new OrderDocument();
order.setCustomerId("customer-1");
order.setStatus(OrderStatus.PENDING);
orderRepository.save(order);
List<OrderDocument> found = orderRepository.findByCustomerId("customer-1");
assertThat(found).hasSize(1);
}
}
When to Use @SpringBootTest
Use @SpringBootTest when you need:
- Multiple layers working together (controller → service → repository)
- Full security context with real JWT validation
- Kafka/RabbitMQ consumer and producer in the same context
- The full Spring Boot auto-configuration chain
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
class OrderApiIT {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:16-alpine");
@Container
@ServiceConnection
static KafkaContainer kafka =
new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.6.1"));
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@Test
void shouldCreateOrderAndPublishEvent() {
// Tests the full path: HTTP → Controller → Service → Repository + Kafka
ResponseEntity<OrderResponse> response = restTemplate.postForEntity(
"http://localhost:" + port + "/api/v1/orders",
new CreateOrderRequest("customer-1", List.of()),
OrderResponse.class
);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
}
}
Test Slice Decision Matrix
| What you’re testing | Slice to use | Testcontainers needed? |
|---|---|---|
| JPA repository query | @DataJpaTest | Yes (with replace=NONE) |
| MongoDB repository | @DataMongoTest | Yes |
| Controller request/response | @WebMvcTest | No (mock services) |
| JSON serialization | @JsonTest | No |
| Service + repository together | @SpringBootTest | Yes |
| Full API flow | @SpringBootTest(webEnvironment=RANDOM_PORT) | Yes |
| Kafka consumer | @SpringBootTest | Yes (KafkaContainer) |
| Security / JWT | @SpringBootTest(webEnvironment=RANDOM_PORT) | Yes (Keycloak or mock JWT) |
Common Pitfalls
@SpringBootTest for everything. Starts the full context for tests that only need one layer. 10x slower than using the appropriate slice.
Forgetting @AutoConfigureTestDatabase(replace = NONE) with @DataJpaTest. Spring Boot silently replaces your PostgreSQL container datasource with H2.
@MockBean in @SpringBootTest breaking context caching. Every different set of @MockBean declarations creates a new application context. If 10 test classes each mock different beans, you get 10 context loads. Group tests that mock the same beans together, or extract a shared configuration.
Summary
Spring Boot test slices give you focused, fast tests at each layer. @DataJpaTest for repositories, @WebMvcTest for controllers, @JsonTest for serialization, and @SpringBootTest when you need the full context. Always combine JPA and MongoDB slices with @AutoConfigureTestDatabase(replace = NONE) and Testcontainers for accurate behavior.
The next article covers singleton containers and shared base classes — the pattern that makes large test suites fast by sharing container instances across the entire JVM.