Testing Spring Boot applications

Full-context versus slice tests, MockMvc, @DataJpaTest, Testcontainers against a real database, and when to mock.

Choosing the right slice

AnnotationLoadsUse for
@SpringBootTestWhole contextA few end-to-end flows, wiring problems
@WebMvcTestMVC layer only, beans mockedController mapping, validation, error shape
@DataJpaTestJPA and repositoriesQueries, mappings, derived methods
Plain JUnitNothingServices 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.
  • @ServiceConnection wires the container's URL, user and password into the context automatically.
  • Keep containers static so 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 @TestConfiguration with a Clock bean so time-dependent logic is deterministic.
  • @Transactional on a test rolls back afterwards, but it also hides flush-timing bugs. Use TestEntityManager.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.

Schema migrations with Flyway and seed data Securing the API with Spring Security

Last refreshed 2026-09-18.