Skip to content

Spring Boot · Testing

Spring Boot Test Slices: @WebMvcTest, @DataJpaTest, and Friends

6 min read Updated 2026-06-26 Share:

Practice Slice Testing interview questions

The middle of the test pyramid

Unit tests skip Spring entirely; full @SpringBootTest boots everything. Slices sit in between: they load only the part of the context relevant to one layer. The web layer. The persistence layer. The JSON layer. You get real framework behavior — routing, SQL, serialization — without paying to start the whole application.

@WebMvcTest(OrderController.class)   // controllers + MVC infra
@DataJpaTest                         // repositories + JPA + embedded DB
@JsonTest                            // Jackson + JSON beans
@RestClientTest(WeatherClient.class) // an HTTP client + mock server

Because each slice registers only its layer's beans and skips the rest, it starts far faster than a full context. Slices should be the majority of your Spring-aware tests.

@WebMvcTest: controllers without a server

@WebMvcTest boots the web layer — controllers, @ControllerAdvice, filters, converters — plus an auto-configured MockMvc. It does not scan your @Service or @Repository beans, so you supply them as mocks:

@WebMvcTest(OrderController.class)
class OrderControllerTest {
    @Autowired MockMvc mvc;
    @MockBean OrderService service;     // controller's dependency, mocked

    @Test void returnsOrder() throws Exception {
        when(service.find(1L)).thenReturn(new Order(1L, "PAID"));
        mvc.perform(get("/orders/1"))
           .andExpect(status().isOk())
           .andExpect(jsonPath("$.status").value("PAID"));
    }
}

Forget the @MockBean and the context fails with "No qualifying bean of type ... OrderService." That's the slice doing its job: it deliberately leaves the lower layers out and expects you to mock them.

MockMvc itself is worth understanding. It drives the DispatcherServlet in-process — no Tomcat, no socket. It runs the full MVC machinery (mapping, argument resolution, the controller, exception handling, JSON rendering) and lets you assert on the outcome:

mvc.perform(post("/orders")
        .contentType(MediaType.APPLICATION_JSON)
        .content("{\"item\":\"widget\"}"))
   .andExpect(status().isCreated())
   .andExpect(header().exists("Location"))
   .andExpect(jsonPath("$.id").isNumber());

Fast and deterministic — but it does not exercise the real servlet container or HTTP stack. For that you need a running server, which is integration-test territory.

Asserting JSON with jsonPath

Don't string-match raw JSON. jsonPath evaluates JSONPath expressions against the body, including proving a field is absent — handy for confirming you never leak a password:

mvc.perform(get("/orders/1"))
   .andExpect(jsonPath("$.id").value(1))
   .andExpect(jsonPath("$.items").isArray())
   .andExpect(jsonPath("$.items.length()").value(3))
   .andExpect(jsonPath("$.secret").doesNotExist());

Security applies in @WebMvcTest

A surprise that bites people: @WebMvcTest does apply your Spring Security filter chain. An unauthenticated request can get a 401/403 and fail the test. Use spring-security-test to supply a user and a CSRF token:

@Test
@WithMockUser(roles = "ADMIN")
void adminCanDelete() throws Exception {
    mvc.perform(delete("/orders/1").with(csrf()))
       .andExpect(status().isNoContent());
}

@WithMockUser injects a principal; .with(csrf()) supplies a valid token for state-changing methods.

@DataJpaTest: repositories against a real database

@DataJpaTest boots the persistence slice — entities, Spring Data repositories, EntityManager, TestEntityManager, and by default an in-memory embedded database. It's @Transactional, so each test rolls back and the DB stays clean between tests:

@DataJpaTest
class OrderRepositoryTest {
    @Autowired OrderRepository repo;
    @Autowired TestEntityManager em;

    @Test void findsByStatus() {
        em.persist(new Order("PAID"));
        assertThat(repo.findByStatus("PAID")).hasSize(1);
    }
}

Note the use of TestEntityManager to set up the data rather than the repository under test. If you used repo.save to seed a test of repo.findByStatus, a broken save could mask a broken find. TestEntityManager also gives you flush and clear so you can force SQL and detach entities, ensuring a read hits the database rather than the first-level cache:

Long id = em.persistAndGetId(new Order("PAID"), Long.class);
em.flush();
em.clear();                                   // detach so the next read hits the DB
Order found = repo.findById(id).orElseThrow();

The H2 trap

By default @DataJpaTest swaps your datasource for H2. Convenient — and a liability. H2 quietly diverges from Postgres on JSON columns, sequences, upserts, and native SQL, giving you "green locally, red in prod." For dialect-sensitive repositories, keep the real engine with Testcontainers:

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
class OrderRepositoryIT {
    @Container @ServiceConnection
    static PostgreSQLContainer<?> db = new PostgreSQLContainer<>("postgres:16");
}

@JsonTest and @RestClientTest

Two narrower slices round things out. @JsonTest boots just Jackson and your configured modules, so you can lock down the exact JSON shape of a DTO using the app's real ObjectMapper:

@JsonTest
class OrderJsonTest {
    @Autowired JacksonTester<Order> json;

    @Test void serializes() throws Exception {
        assertThat(json.write(new Order(1L, "PAID")))
            .extractingJsonPathStringValue("$.status").isEqualTo("PAID");
    }
}

@RestClientTest slices in the beans for an outbound API client plus a MockRestServiceServer that stubs the remote endpoint — so you can test URL building, headers, parsing, and error handling without hitting the live third party:

@RestClientTest(WeatherClient.class)
class WeatherClientTest {
    @Autowired WeatherClient client;
    @Autowired MockRestServiceServer server;

    @Test void parsesResponse() {
        server.expect(requestTo("/weather?city=NYC"))
              .andRespond(withSuccess("{\"temp\":21}", MediaType.APPLICATION_JSON));
        assertThat(client.tempFor("NYC")).isEqualTo(21);
    }
}

Keep the context cache happy

@MockBean is how slices receive collaborators, but it has a cost: each distinct set of mocked beans is a different context configuration that Spring caches separately. Spawn dozens of unique combinations and you defeat context caching, and the suite slows to a crawl. Keep the set of mocks consistent across tests in a slice. (Boot 3.4+ offers @MockitoBean as the replacement for the deprecated @MockBean.)

When a slice excludes something you actually need — a custom converter, a @ControllerAdvice that isn't scanned — widen it surgically rather than escalating to a full @SpringBootTest:

@WebMvcTest(OrderController.class)
@Import(JacksonConfig.class)                 // bring in a specific @Configuration
@AutoConfigureMockMvc(addFilters = false)    // skip security filters if desired
class T { /* ... */ }

The pitfalls, collected

  • Using @SpringBootTest when a slice would do — slow tests, no extra coverage.
  • Forgetting a @MockBean for a controller dependency.
  • Assuming @WebMvcTest ignores security — it doesn't.
  • Trusting H2 in @DataJpaTest for Postgres-specific SQL.
  • Seeding @DataJpaTest through the repo under test instead of TestEntityManager.
  • Fragmenting the context cache with many unique @MockBean combinations.

Recap

Slices load one layer so you get real framework wiring without booting the whole app. @WebMvcTest + MockMvc tests controllers (mock the services, mind security, assert with jsonPath); @DataJpaTest tests repositories against a database (set up with TestEntityManager, and don't trust H2 for dialect-specific SQL); @JsonTest and @RestClientTest cover serialization and API clients. Keep your @MockBean sets consistent to preserve context caching, and reach for the narrowest slice that proves the behavior.

More ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel