Testing Spring Boot Applications
Spring Boot gives you fast unit tests, focused slice tests and full integration tests. Choose the smallest test that proves the behavior, because slice tests start much faster than full application tests.
Overview
| Test type | Annotation | Loads | Speed |
|---|---|---|---|
| Unit test | @ExtendWith(MockitoExtension.class) | Nothing from Spring | Very fast |
| Web slice | @WebMvcTest | Controllers, advice, converters only | Fast |
| Data slice | @DataJpaTest | JPA repositories and an in-memory DB | Fast |
| Full integration | @SpringBootTest | The whole application context | Slower |
spring-boot-starter-test brings JUnit 5, Mockito, AssertJ, Hamcrest, JsonPath and MockMvc. Note: in Spring Boot 4 the test annotations and auto-configure classes moved into modular packages, so check the imports of your version. The names and ideas stay the same.
1. Unit test of a service (no Spring)
@ExtendWith(MockitoExtension.class)
class ProductServiceTest {
@Mock ProductRepository repository;
@InjectMocks ProductService service;
@Test
void createRejectsDuplicateName() {
when(repository.existsByNameIgnoreCase("Pen")).thenReturn(true);
assertThatThrownBy(() -> service.create(new ProductRequest("Pen", BigDecimal.TEN)))
.isInstanceOf(ConflictException.class)
.hasMessageContaining("already exists");
verify(repository, never()).save(any());
}
}2. Controller test with MockMvc
// Boot 3.x import: org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
@WebMvcTest(ProductController.class)
class ProductControllerTest {
@Autowired MockMvc mvc;
@MockitoBean ProductService service; // @MockBean in older versions (removed in Boot 4)
@Test
void getReturnsProduct() throws Exception {
when(service.get(1L)).thenReturn(new ProductResponse(1L, "Pen", BigDecimal.TEN));
mvc.perform(get("/api/products/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Pen"));
}
@Test
void createValidatesInput() throws Exception {
mvc.perform(post("/api/products")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"\",\"price\":-5}"))
.andExpect(status().isBadRequest());
}
}3. Repository test
@DataJpaTest
class ProductRepositoryTest {
@Autowired ProductRepository repository;
@Test
void findsByName() {
repository.save(new Product("Pen", BigDecimal.TEN));
assertThat(repository.existsByNameIgnoreCase("pen")).isTrue();
}
}4. Full integration test with a real database (Testcontainers)
@SpringBootTest
@AutoConfigureMockMvc
@Testcontainers
class ProductApiIT {
@Container
@ServiceConnection // Boot wires the datasource to the container automatically
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");
@Autowired MockMvc mvc;
@Test
void createAndReadProduct() throws Exception {
mvc.perform(post("/api/products").contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"Laptop\",\"price\":55000}"))
.andExpect(status().isCreated());
mvc.perform(get("/api/products"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.content[0].name").value("Laptop"));
}
}Testcontainers runs real PostgreSQL, Kafka or Redis in Docker for the test. This finds problems that H2 cannot show, because H2 behaves differently from PostgreSQL.
Useful test settings
@ActiveProfiles("test") // loads application-test.yml
@TestPropertySource(properties = "app.feature.enabled=true")
@DirtiesContext // rebuild the context after this test (slow, use rarely)- Name unit tests *Test and integration tests *IT so Maven can run them separately (surefire and failsafe).
- Test behavior, not implementation. Do not verify every internal call.
- The Spring context is cached between tests with the same configuration. Different @MockitoBean sets create new contexts and slow the build.
How the Spring test context cache works
Test class A @SpringBootTest ─► context #1 created (slow: 3 to 10 s) stored in the cache
Test class B @SpringBootTest (same configuration) ─► context #1 REUSED (fast)
Test class C @SpringBootTest @ActiveProfiles("x") ─► different key ─► context #2 created
Test class D @SpringBootTest + @MockitoBean Foo ─► different key ─► context #3 created
Cache key = configuration classes + active profiles + properties (@TestPropertySource, properties=) + context initializers + mock/spy beans + web environment ...
Every different combination = one more application startup. Many variants make the build slow.- Put common @MockitoBean declarations in one shared abstract base class or configuration, so many tests share one context.
- @DirtiesContext throws the context away after the test. Use it only when a test really changes global state.
- @DataJpaTest, @WebMvcTest and other slices build small contexts that are cached separately.
Dynamic properties for containers
@SpringBootTest
@Testcontainers
class RedisIT {
@Container
static GenericContainer<?> redis = new GenericContainer<>("redis:7").withExposedPorts(6379);
@DynamicPropertySource
static void redisProperties(DynamicPropertyRegistry registry) {
registry.add("spring.data.redis.host", redis::getHost);
registry.add("spring.data.redis.port", () -> redis.getMappedPort(6379));
}
}More test slices
| Annotation | Loads | Use for |
|---|---|---|
| @WebMvcTest | Controllers, advice, filters, converters (no services) | HTTP behavior with MockMvc |
| @DataJpaTest | Entities, repositories, embedded DB, rollback | Queries and mappings |
| @JdbcTest | DataSource and JdbcTemplate | Plain SQL code |
| @JsonTest | Jackson configuration | JSON shape of DTOs |
| @RestClientTest | RestClient and MockRestServiceServer | Outgoing HTTP clients |
| @DataMongoTest / @DataRedisTest | Store specific slices | Data access with Testcontainers |
| @SpringBootTest | Everything | End-to-end flows, few of them |
@JsonTest
class ProductResponseJsonTest {
@Autowired JacksonTester<ProductResponse> json;
@Test
void serializes() throws Exception {
JsonContent<ProductResponse> result = json.write(new ProductResponse(1L, "Pen", new BigDecimal("10.50")));
assertThat(result).extractingJsonPathStringValue("$.name").isEqualTo("Pen");
assertThat(result).extractingJsonPathNumberValue("$.price").isEqualTo(10.50);
}
}Test asynchronous behavior with Awaitility (no Thread.sleep)
@Test
void listenerProcessesEvent() {
publisher.publish(new OrderPlaced("o-1", "c-1", BigDecimal.TEN));
await().atMost(Duration.ofSeconds(5)).pollInterval(Duration.ofMillis(100))
.untilAsserted(() -> assertThat(invoiceRepository.findByOrderId("o-1")).isPresent());
}Test data builders keep tests readable
public class ProductBuilder {
private String name = "Pen";
private BigDecimal price = BigDecimal.TEN;
public static ProductBuilder aProduct() { return new ProductBuilder(); }
public ProductBuilder named(String name) { this.name = name; return this; }
public ProductBuilder priced(String price) { this.price = new BigDecimal(price); return this; }
public Product build() { return new Product(name, price, "general"); }
}
// Product laptop = aProduct().named("Laptop").priced("55000").build();What makes tests flaky, and the fix
| Cause | Fix |
|---|---|
| Thread.sleep and timing | Awaitility with a condition |
| Shared database state between tests | Cleanup in @BeforeEach or rollback, unique test data |
| Current time and randomness | Inject a Clock and a random source |
| Order dependence | Each test creates its own data |
| Real external services | WireMock, MockRestServiceServer, Testcontainers |
| Ports in use | RANDOM_PORT |
Common mistake: Using @SpringBootTest for every test. The whole application starts each time and the test suite becomes very slow. Prefer unit tests and slice tests.
Interview questions
What is a test slice?
An annotation such as @WebMvcTest or @DataJpaTest that loads only the part of the application needed for that layer.
@Mock or @MockitoBean?
@Mock is plain Mockito for unit tests without Spring. @MockitoBean replaces a bean inside the Spring context for slice and integration tests.
Why Testcontainers instead of H2?
It tests against the same database engine you use in production, so SQL dialect and constraint differences are caught early.
Why are my Spring tests slow?
Usually because many different context configurations each start the application (different mocks, profiles, properties) and because @SpringBootTest is used where a slice or a unit test would do.
Should I mock the repository or use a real database?
Mock it in service unit tests to test logic. Use @DataJpaTest with a real database (Testcontainers) to test queries and mappings.