Slice Tests and Testcontainers
Definition
A slice test loads only one part of the application, for example @WebMvcTest for controllers or @DataJpaTest for repositories. Testcontainers starts real services such as a database in Docker for your tests.
Why it exists
In-memory stand-ins like H2 behave differently from production databases, and full-application tests are slow. Slices stay fast, and Testcontainers gives you the real database.
How it works
Slice annotations
| Annotation | Loads | Typical use |
|---|---|---|
@WebMvcTest(Controller.class) | MVC infrastructure, controllers, advice, filters, converters (not services) | Request mapping, JSON, validation, status codes |
@DataJpaTest | Entities, repositories, Hibernate, transactions (each test rolls back) | Queries, mappings, constraints |
@JsonTest | Jackson ObjectMapper and JSON tester | Serialization rules |
@RestClientTest | RestClient/RestTemplate with mock server | Outbound HTTP clients |
@SpringBootTest | Everything | A few end-to-end flows |
Example code
@WebMvcTest example
@WebMvcTest(OrderController.class)
class OrderControllerTest {
@Autowired MockMvc mvc;
@MockitoBean OrderService orderService; // Spring Framework 6.2+, Boot 3.4+
@Test
void returns404WhenOrderMissing() throws Exception {
when(orderService.find(42L)).thenThrow(new OrderNotFoundException(42L));
mvc.perform(get("/orders/42"))
.andExpect(status().isNotFound())
.andExpect(jsonPath("$.orderId").value(42));
}
@Test
void rejectsEmptyBody() throws Exception {
mvc.perform(post("/orders")
.contentType(MediaType.APPLICATION_JSON)
.content("{}"))
.andExpect(status().isBadRequest());
}
}
The service layer is replaced with a mock, so the test checks only the web contract: URL mapping, JSON binding, validation and the exception handler from exception handling. If Spring Security is on the classpath, MVC slice tests apply it too; use @WithMockUser or .with(csrf()) as needed.
@DataJpaTest with Testcontainers
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
class OrderRepositoryTest {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
@Autowired OrderRepository repository;
@Test
void findsOrdersByCustomer() {
repository.save(new OrderEntity(1L, "C-1"));
assertThat(repository.findByCustomerId("C-1")).hasSize(1);
}
}
Key parts:
@Containeron astaticfield means one container per test class; the@Testcontainersextension starts and stops it.@ServiceConnection(Boot 3.1+) auto-configures theDataSourcefrom the container, replacing manual@DynamicPropertySourcewiring.Replace.NONEstops Boot swapping in an embedded database.
For other services use the matching container and @ServiceConnection: KafkaContainer, GenericContainer with @DynamicPropertySource for anything without built-in support.
Sharing one container across many classes
Starting a container per class is slow. Use an abstract base class with a static singleton container started once, or Testcontainers' reuse feature in local development.
abstract class AbstractIntegrationTest {
@ServiceConnection
static final PostgreSQLContainer<?> POSTGRES = new PostgreSQLContainer<>("postgres:16-alpine");
static {
POSTGRES.start(); // stopped by Ryuk when the JVM exits
}
}
Full-stack test
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class OrderFlowIT extends AbstractIntegrationTest {
@Autowired TestRestTemplate rest;
@Test
void createAndFetchOrder() {
var created = rest.postForEntity("/orders", new CreateOrderRequest(1L, List.of()), Void.class);
assertThat(created.getStatusCode().is2xxSuccessful()).isTrue();
}
}
Reserve these for a handful of critical journeys. Name them *IT and run them with the Failsafe plugin so mvn test stays fast.
System design considerations
- Reuse containers across tests and run migrations in the test setup
- Pin container image versions to match production
- Keep an end-to-end smoke test against a production-like environment
Boot 4 note
Spring Boot 4 splits test support into per-technology modules, and annotations such as @WebMvcTest live in new packages with their own test starters (for example spring-boot-starter-webmvc-test). The concepts are unchanged; check imports and starters when upgrading.
When to use it, and when not
Use it when
- You verify controllers, repositories or serialisation in isolation
- You test SQL, migrations and locking against the real database
- You need Kafka or Redis in integration tests
Think twice when
- The check is pure logic that a unit test covers faster
- CI cannot run Docker and you have no alternative
- You use full-stack tests for everything
Why not H2?
An in-memory H2 database behaves differently from PostgreSQL or MySQL: SQL dialect, locking, JSON columns, sequences, case rules. Tests pass and production fails. Testcontainers starts the same database in Docker for the duration of the test.
Common mistakes
@SpringBootTestfor everything; the suite takes minutes.@MockitoBeanwith different mock sets on each class, defeating context caching.- Docker not available on CI runners; use a runner with Docker or a Testcontainers cloud service.
- Hibernate
ddl-auto=createin tests while production uses migrations; run the real migrations instead.
Interview questions
1. What is a test slice?
A Boot test annotation that builds a minimal application context for one layer, with relevant auto-configuration only. It is faster and more focused than @SpringBootTest.
2. Why does @WebMvcTest need @MockitoBean for services?
Services are not loaded in that slice. The controller's dependencies must be supplied, and a mock isolates the web layer.
3. Why Testcontainers rather than H2?
Fidelity. You test against the same engine and SQL dialect that production runs, including migrations (Flyway/Liquibase) and engine-specific features.
4. What does @ServiceConnection do?
It maps a container to Boot's connection details (JDBC URL, credentials, Kafka bootstrap servers) automatically, so no property juggling.
5. How do you keep integration tests fast?
Reuse containers across classes, share the Spring context (identical configuration is cached), use slices where possible and avoid @DirtiesContext.
6. How is test data isolated?
@DataJpaTest and @Transactional tests roll back after each test. For full-stack tests with real HTTP the transaction is not shared, so clean data explicitly or use unique identifiers.
Check yourself
1Which test slice loads only the web layer?
2Why prefer Testcontainers over H2 for repository tests?
3What does @SpringBootTest load by default?
Key takeaways
- Pick the smallest slice that covers the behaviour.
- Test against real infrastructure with Testcontainers and
@ServiceConnection. - Keep a small number of full-context tests, and keep everything else fast.
Finished this page? Track your progress (saved only in your browser).