Spring Boot NotesAdvanced
Testing Spring Boot Applications
Unit tests, @WebMvcTest, @DataJpaTest, and full integration tests with Testcontainers.
Testing Spring Boot Applications
The Testing Pyramid in a Spring Boot Context
- Unit tests — plain JUnit + Mockito, no Spring context loaded. Fast.
- Slice tests — load only part of the context (
@WebMvcTest,@DataJpaTest). Fast-ish. - Integration tests —
@SpringBootTest, full context, possibly a real (containerized) database. Slower, fewer of these.
Unit Testing a Service with Mockito
@ExtendWith(MockitoExtension.class)
class TransactionServiceTest {
@Mock
private TransactionRepository repository;
@InjectMocks
private TransactionService service;
@Test
void shouldThrowWhenTransactionNotFound() {
when(repository.findById(1L)).thenReturn(Optional.empty());
assertThrows(ResourceNotFoundException.class, () -> service.findById(1L));
}
@Test
void shouldCalculateTotalCorrectly() {
when(repository.findByUserId(1L)).thenReturn(List.of(
new Transaction(BigDecimal.valueOf(100)),
new Transaction(BigDecimal.valueOf(50))
));
BigDecimal total = service.getTotalForUser(1L);
assertEquals(BigDecimal.valueOf(150), total);
}
}No Spring context is loaded here — it's plain JUnit 5 + Mockito, so it runs in milliseconds.
@WebMvcTest — Testing the Web Layer Only
@WebMvcTest(TransactionController.class)
class TransactionControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private TransactionService service;
@Test
void shouldReturn404WhenNotFound() throws Exception {
when(service.findById(1L)).thenThrow(new ResourceNotFoundException("not found"));
mockMvc.perform(get("/api/transactions/1"))
.andExpect(status().isNotFound());
}
@Test
void shouldCreateTransaction() throws Exception {
mockMvc.perform(post("/api/transactions")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"description":"Coffee","amount":150,"date":"2026-07-01","category":"Food"}
"""))
.andExpect(status().isCreated());
}
}Loads only MVC infrastructure + the specified controller — the service is mocked, so no database is touched.
@DataJpaTest — Testing the Repository Layer
@DataJpaTest
class TransactionRepositoryTest {
@Autowired
private TransactionRepository repository;
@Test
void shouldFindByDateRange() {
repository.save(new Transaction(..., LocalDate.of(2026, 1, 10)));
List<Transaction> result = repository.findByDateBetween(
LocalDate.of(2026, 1, 1), LocalDate.of(2026, 1, 31));
assertThat(result).hasSize(1);
}
}By default uses an in-memory H2 database and rolls back after each test.
@SpringBootTest — Full Integration Test
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
class TransactionIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");
@DynamicPropertySource
static void configureProps(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
}
@Autowired
private TestRestTemplate restTemplate;
@Test
void shouldCreateAndFetchTransaction() {
// exercises the real HTTP stack against a real Postgres container
}
}Testcontainers spins up a real, disposable database in Docker for integration tests — far more reliable than testing against H2 and hoping SQL dialect differences don't bite you in production.
Practical Guidance
- Most of your test suite should be fast unit tests on services.
- A handful of
@WebMvcTest/@DataJpaTestslice tests per feature. - A small number of true end-to-end
@SpringBootTesttests for critical flows (login, payment/transaction creation) — they're valuable but expensive to run, so don't over-invest here.
Last updated on July 15, 2026