Spring Boot test slices WebMvcTest DataJpaTest MockMvc production testing
Md Sanwar Hossain
Md Sanwar Hossain
Senior Software Engineer · Spring Boot Testing Series
Testing April 4, 2026 20 min read Spring Boot Testing Series

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

  1. What Are Test Slices and Why They Matter
  2. @WebMvcTest: Controller Layer Testing with MockMvc
  3. MockMvc Patterns: Request Building, JSON Assertions, and Security
  4. @DataJpaTest: Repository Testing with Real SQL
  5. Combining @DataJpaTest with Testcontainers
  6. @RestClientTest: Testing HTTP Clients Without Hitting Real Endpoints
  7. Other Slices: @DataMongoTest, @DataRedisTest, @JsonTest
  8. Creating a Custom Test Slice for Your Domain
  9. Key Takeaways

1. What Are Test Slices and Why They Matter

Spring Boot test slices architecture overview | mdsanwarhossain.me
Spring Boot Test Slices Architecture — mdsanwarhossain.me

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 Spring Boot security testing patterns | mdsanwarhossain.me
MockMvc Spring Security Testing — mdsanwarhossain.me

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

Leave a Comment

Related Posts

Md Sanwar Hossain - Software Engineer
Md Sanwar Hossain

Software Engineer · Java · Spring Boot · Microservices

Last updated: April 4, 2026