Backend engineer path · Data and tests · Topic 12 of 18

Testing with slices, @SpringBootTest and Testcontainers

Not started

Spring Boot's test support is about choosing how much of the application to start. A plain unit test starts nothing. A **slice** such as @WebMvcTest or @DataJpaTest starts one layer with only the auto-configuration it needs. @SpringBootTest starts the whole context, and can start the embedded server too. Testcontainers adds real infrastructure, such as PostgreSQL in Docker, so the tests that matter run against the same database as production.

Why it matters: A suite that boots the whole application for every check gets slow enough that people stop running it; one that never touches a real database misses the bugs that matter.

  1. 1Plain tests first

    Constructor injection means most logic can be tested with new and a few fakes, with no Spring at all.

  2. 2Slices load one layer

    @WebMvcTest starts only the MVC layer; @DataJpaTest starts only JPA and repositories.

  3. 3@SpringBootTest loads everything

    The full context, optionally with a real server on a random port, for end-to-end checks of the wiring.

  4. 4Real dependencies in containers

    Testcontainers with @ServiceConnection runs a real PostgreSQL or Kafka in Docker and points Boot at it.

1

How it works

The explanation, with examples you can run.

The web slice

@WebMvcTest(OrderController.class)class OrderControllerTest {  @Autowired MockMvc mvc;  @MockitoBean OrderService orders;     // replaces the real bean in this context   @Test  void rejectsEmptyOrder() throws Exception {    mvc.perform(post("/orders/7")            .contentType(MediaType.APPLICATION_JSON)            .content("{\"items\":[]}"))        .andExpect(status().isBadRequest());  }}

@WebMvcTest limits scanning to web components (controllers, @ControllerAdvice, filters, converters, interceptors, WebMvcConfigurers) and skips @Service, @Repository and ordinary @Configuration classes; collaborators are mocks. Spring Security's auto-configuration still applies, so @Import your own security configuration if the test should use its rules. MockMvc sends requests through the real DispatcherServlet without opening a socket. @MockitoBean comes from Spring Framework 6.2; the older @MockBean from Boot is deprecated since Boot 3.4.

The JPA slice

@DataJpaTest@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)@Testcontainersclass OrderRepositoryTest {  @Container @ServiceConnection  static PostgreSQLContainer<?> db = new PostgreSQLContainer<>("postgres:16");   @Autowired OrderRepository repo;   @Test  void findsRecentOrdersWithItems() { ... }}

@DataJpaTest configures entities, repositories and a TestEntityManager; each test runs in a transaction that is rolled back. On its own it swaps in an embedded database if one is on the classpath. Replace.NONE keeps the real one, here a container that @ServiceConnection wires in automatically. Testing against the same database engine as production catches dialect differences, real constraint behaviour and the N+1 query counts an in-memory database would hide.

The whole application

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)@Testcontainersclass CheckoutFlowTest {  @Container @ServiceConnection  static PostgreSQLContainer<?> db = new PostgreSQLContainer<>("postgres:16");   @Autowired TestRestTemplate http;   @Test  void placesAnOrder() {    var res = http.postForEntity("/orders/7", newOrder(), OrderView.class);    assertThat(res.getStatusCode()).isEqualTo(HttpStatus.CREATED);  }}

Without webEnvironment, @SpringBootTest uses a mock web environment and starts no server. RANDOM_PORT starts the real embedded server, so filters, security, serialisation and transactions behave exactly as in production. One trap here: the test and the server run on different threads, so a @Transactional on the test method does not roll back what the server committed.

Keep the suite fast

Spring caches application contexts between test classes that use the same configuration. Every distinct combination of @MockitoBeans, properties or profiles creates a new context, so a suite with twenty slightly different set-ups starts twenty applications. Share a few base configurations, keep most tests as plain unit tests, and save @SpringBootTest for the flows that need everything.

2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.You want to check that POST /orders returns 400 for an invalid body. Which test is the best fit?
  2. 2.By default, what happens to rows a @DataJpaTest test inserts?
  3. 3.What does @ServiceConnection on a @Container PostgreSQLContainer field do?

0 of 3 right so far. Get all three to finish this topic.