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
  • @DataJpaTest for JPA repositories
  • @WebMvcTest for Spring MVC controllers
  • @JsonTest for JSON serialization/deserialization
  • @DataMongoTest for 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:

  1. Start only a specific subset of the Spring application context
  2. Auto-configure only the relevant infrastructure
  3. 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 testingSlice to useTestcontainers needed?
JPA repository query@DataJpaTestYes (with replace=NONE)
MongoDB repository@DataMongoTestYes
Controller request/response@WebMvcTestNo (mock services)
JSON serialization@JsonTestNo
Service + repository together@SpringBootTestYes
Full API flow@SpringBootTest(webEnvironment=RANDOM_PORT)Yes
Kafka consumer@SpringBootTestYes (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.

Next: Singleton Containers and Shared Base Classes