A good test suite gives you the confidence to refactor. Spring Boot supports a layered approach: fast unit tests at the base, focused slice tests in the middle, and a few full integration tests at the top.
Plain unit tests
The fastest tests need no Spring context at all — construct the class and pass in mocks.
class PriceCalculatorTest {
@Test
void appliesDiscount() {
var calculator = new PriceCalculator();
var result = calculator.apply(new BigDecimal("100"), 0.1);
assertEquals(new BigDecimal("90.0"), result);
}
}Slice tests
Slice annotations load only part of the context. @WebMvcTest boots the web
layer without touching the database.
@WebMvcTest(ProductController.class)
class ProductControllerTest {
@Autowired MockMvc mvc;
@MockBean ProductService service;
@Test
void returnsProduct() throws Exception {
when(service.findById(1L)).thenReturn(new ProductResponse(1L, "Book", TEN));
mvc.perform(get("/api/products/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Book"));
}
}Integration tests with Testcontainers
For confidence that your queries actually work, run tests against a real database in a throwaway container.
@SpringBootTest
@Testcontainers
class OrderRepositoryIT {
@Container
static PostgreSQLContainer<?> db = new PostgreSQLContainer<>("postgres:16");
@DynamicPropertySource
static void props(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", db::getJdbcUrl);
registry.add("spring.datasource.username", db::getUsername);
registry.add("spring.datasource.password", db::getPassword);
}
}Keep the pyramid balanced
- Many unit tests — milliseconds each.
- Some slice tests — one layer at a time.
- Few integration tests — realistic but slower.
This shape keeps your feedback loop fast while still exercising the real stack where it counts.