@SpringBootTest loads the entire application context: every bean, every auto-configuration, the works. That's correct for genuine end-to-end tests, but using it as the default for every test class is the single biggest reason test suites end up taking twenty minutes to run. Test slices exist to load exactly the beans a given layer needs, and using them deliberately is one of the highest-leverage habits in a Spring codebase.
What a Slice Actually Loads
@WebMvcTest boots the web layer only — controllers, @ControllerAdvice, filters, Jackson configuration, Bean Validation — and explicitly excludes @Service and @Repository beans. Anything a controller depends on has to be mocked.
@WebMvcTest(OrderController.class)
class OrderControllerTest {
@Autowired MockMvc mockMvc;
@MockBean OrderService orderService;
@Test
void returnsOrderById() throws Exception {
when(orderService.findById(1L)).thenReturn(new OrderDto(1L, "SHIPPED"));
mockMvc.perform(get("/api/orders/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("SHIPPED"));
}
}This test starts in a fraction of a second compared to a full context, because Hibernate, the connection pool, and every unrelated @Service never get instantiated. It's also a more honest unit test of the controller: it verifies request mapping, serialization, and validation without pretending to test the service layer at the same time.
@DataJpaTest does the mirror image for persistence — it configures an embedded database (or a Testcontainers-backed real one, with the right setup), @Entity scanning, and Spring Data repositories, while skipping the web layer and most @Service beans entirely.
@DataJpaTest
class OrderRepositoryTest {
@Autowired TestEntityManager entityManager;
@Autowired OrderRepository orderRepository;
@Test
void findsOrdersByStatus() {
entityManager.persist(new Order("SHIPPED"));
entityManager.persist(new Order("PENDING"));
List<Order> shipped = orderRepository.findByStatus("SHIPPED");
assertThat(shipped).hasSize(1);
}
}By default @DataJpaTest uses an in-memory database and rolls back each test in a transaction, which is fast but means it isn't exercising the actual SQL dialect your production database speaks — a query that works on H2 can fail on Postgres. Pairing it with @Testcontainers and a real Postgres container closes that gap at a modest cost in startup time.
Where Slices Stop Making Sense
Slices earn their keep when a test genuinely only needs one layer. They stop being useful — and start actively hiding bugs — once a test's real purpose is to verify that layers work together: that a request through the full filter chain, into a real service, hitting a real transaction boundary, produces the right database state. Mocking the service in a @WebMvcTest can't catch a bug where the controller and service disagree about who owns validation, or where a @Transactional boundary is in the wrong place.
A reasonable rule of thumb: use @WebMvcTest and @DataJpaTest for the bulk of your test suite, where each test has a narrow, single-layer responsibility, and reserve a smaller number of @SpringBootTest(webEnvironment = RANDOM_PORT) tests for the critical paths that need to prove the whole stack is wired correctly — checkout, authentication, payment capture. If most of your test suite is full-context tests "just to be safe," you're paying the startup cost on every test without getting proportionally more confidence, and the suite's runtime will eventually train the team to run it less often, which defeats the point of having it.