Testing with Spring Boot test slices
When to start the whole application and when to start a slice of it, what each slice loads, and how the test context cache makes or breaks suite speed.
After this lesson you can
- Choose between a plain unit test, a slice test and @SpringBootTest
- Say which beans @WebMvcTest and @DataJpaTest load, and how to supply the rest
- Keep the context cache effective, and predict how many contexts a suite starts
- Predict which test data a rolled-back test transaction does and does not undo
A Spring Boot application can be tested at three levels, and a good suite uses all of them.
- Plain unit tests —
new OrderService(fakeRepository, fixedClock)with JUnit and Mockito, no Spring at all. Constructor injection makes this possible, and it is where most business logic should be tested. - Slice tests — start only the part of the application one layer needs, with the matching auto-configuration.
@SpringBootTest— start the whole application context. By default with a mock servlet environment; withwebEnvironment = RANDOM_PORTa real server on a random port, called throughTestRestTemplateorWebTestClient.
The slices
@WebMvcTest(OrderController.class) loads the Spring MVC
infrastructure and web components — @Controller/@RestController,
@ControllerAdvice, Jackson, converters, filters, WebMvcConfigurer
— and auto-configures MockMvc. It does not load @Service,
@Repository or plain @Component beans, so the controller's
collaborators must be supplied as mocks:
@WebMvcTest(OrderController.class)
class OrderControllerTest {
@Autowired MockMvc mvc;
@MockitoBean OrderService orders; // Spring Framework 6.2 / Boot 3.4+; Boot's @MockBean is deprecated
@Test
void returns404ForUnknownOrder() throws Exception {
when(orders.find(42L)).thenThrow(new OrderNotFoundException(42L));
mvc.perform(get("/orders/42")).andExpect(status().isNotFound());
}
}
@DataJpaTest loads entities, Spring Data repositories, the
EntityManager and a TestEntityManager; each test runs in a
transaction that is rolled back at the end. By default it replaces
the application's DataSource with an embedded in-memory database;
@AutoConfigureTestDatabase(replace = NONE) keeps the real one, which
with Testcontainers and @ServiceConnection (Boot 3.1+) gives you the
same database engine as production. Other slices: @JsonTest,
@RestClientTest, @WebFluxTest, @DataJdbcTest.
The rollback is not specific to @DataJpaTest: any Spring test method
annotated @Transactional runs in a test-managed transaction that is
rolled back afterwards, and @Commit keeps its data instead. It only
covers work done on the test's own thread. With
webEnvironment = RANDOM_PORT, requests are handled by a real server
on other threads, in their own transactions, so what they write is
committed and stays for the tests that follow.
import java.util.*; public class Solution { record Bean(String name, String stereotype) {} static final Map<String, Set<String>> SLICES = Map.of( "@WebMvcTest", Set.of("RestController", "ControllerAdvice", "Filter", "WebMvcConfigurer"), "@DataJpaTest", Set.of("Entity", "Repository"), "@SpringBootTest", Set.of("RestController", "ControllerAdvice", "Filter", "WebMvcConfigurer", "Entity", "Repository", "Service", "Component")); public static List<String> run() { List<Bean> app = List.of( new Bean("OrderController", "RestController"), new Bean("ApiErrors", "ControllerAdvice"), new Bean("OrderService", "Service"), new Bean("OrderRepository", "Repository"), new Bean("Order", "Entity"), new Bean("PriceCalculator", "Component")); List<String> out = new ArrayList<>(); for (String slice : List.of("@WebMvcTest", "@DataJpaTest", "@SpringBootTest")) { List<String> loaded = new ArrayList<>(); for (Bean b : app) if (SLICES.get(slice).contains(b.stereotype())) loaded.add(b.name()); out.add(slice + ": " + loaded); } return out; }}The context cache
Starting a context is the slow part of a Spring test. The TestContext
framework therefore caches contexts, keyed by their configuration:
the configuration classes, active profiles, properties, the set of
@MockitoBeans and more. Two test classes with identical configuration
share one context; any difference — one extra mocked bean, one extra
property — means another context. The cache holds at most 32 contexts
by default (spring.test.context.cache.maxSize) and evicts the
least recently used one. @DirtiesContext throws a context away
deliberately.
The practical rule: standardise test configuration (a shared base
class or a few meta-annotations) so classes reuse contexts, and treat
@DirtiesContext as a smell.
Common mistakes
@SpringBootTestfor every test, so a suite that should take seconds starts the whole application dozens of times.- Expecting a
@Serviceto be injected in a@WebMvcTest: it is not in the slice, and the context fails to start until it is mocked. - Each test class mocking a slightly different set of beans, which defeats the context cache.
- Testing repositories against H2 while production runs PostgreSQL, then shipping a query that only H2 accepts.
What interviewers ask
@WebMvcTestversus@SpringBootTest? — the web slice with mocked collaborators andMockMvc, versus the whole context; speed and isolation versus end-to-end wiring.
Exercise · Pro
What the next test sees
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade