Testing Spring Boot applications
Full-context versus slice tests, MockMvc, @DataJpaTest, Testcontainers against a real database, and when to mock.
Choosing the right slice
| Annotation | Loads | Use for |
|---|---|---|
@SpringBootTest | Whole context | A few end-to-end flows, wiring problems |
@WebMvcTest | MVC layer only, beans mocked | Controller mapping, validation, error shape |
@DataJpaTest | JPA and repositories | Queries, mappings, derived methods |
| Plain JUnit | Nothing | Services and domain logic - the fastest tests |
@WebMvcTest(BookController.class)
class BookControllerTest {
@Autowired MockMvc mvc;
@MockitoBean BookService service;
@Test
void rejectsBlankTitle() throws Exception {
mvc.perform(post("/books").contentType(APPLICATION_JSON)
.content("""
{"title": "", "author": "Le Guin"}
"""))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errors[0].field").value("title"));
}
}💡
@MockitoBean replaced the deprecated @MockBean in Spring Boot 3.4. Use it in slice tests; use real collaborators in the few tests where integration behaviour is the point.Integration tests against a real database
@Testcontainers
@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
class BookRepositoryIT {
@Container @ServiceConnection
static PostgreSQLContainer<?> db = new PostgreSQLContainer<>("postgres:16-alpine");
@Autowired BookRepository repo;
@Test
void findsRecentBooksWithAuthor() {
var since = Instant.now().minus(7, ChronoUnit.DAYS);
assertThat(repo.findRecentWithDetails(since))
.allSatisfy(b -> assertThat(b.getAuthor()).isNotNull());
}
}- In-memory H2 accepts SQL PostgreSQL would reject, so an integration test on H2 can pass while production fails. Run integration tests on the same engine as production.
@ServiceConnectionwires the container's URL, user and password into the context automatically.- Keep containers
staticso they start once per class rather than per test method.
// transaction boundary check in a @SpringBootTest
@Test
void rollbackOnFailure() {
assertThatThrownBy(() -> service.transferWithoutFunds(1L, 2L, HUNDRED))
.isInstanceOf(IllegalStateException.class);
assertThat(accounts.balanceOf(1L)).isEqualByComparingTo(TEN);
}What to assert and what to mock
- Assert on observable behaviour - HTTP status, response body, resulting row - not on how many times a private collaborator was called.
- Mock at the boundary you do not own: payment providers, email senders, clocks. Prefer a real database over a mocked repository.
- Use
@TestConfigurationwith aClockbean so time-dependent logic is deterministic. @Transactionalon a test rolls back afterwards, but it also hides flush-timing bugs. UseTestEntityManager.flush()when the constraint is the point of the test.
@TestConfiguration
class TestClock {
@Bean Clock clock() { return Clock.fixed(Instant.parse("2026-01-01T00:00:00Z"), ZoneOffset.UTC); }
}FAQ
Why is my @SpringBootTest so slow?
Each distinct context configuration gets its own cached context, and different
@MockitoBean sets create different caches. Share one configuration across the suite and use slice tests for the rest.Do I need both MockMvc and Testcontainers?
They answer different questions. MockMvc verifies the HTTP contract quickly; Testcontainers verifies that your SQL and mappings work on the real engine. Most services need a handful of each, not hundreds.
Related
Schema migrations with Flyway and seed data Securing the API with Spring Security
Last refreshed 2026-09-18.