Testing Spring Apps
@SpringBootTest, MockMvc, @DataJpaTest, Testcontainers, WireMock
Spring Boot provides excellent testing support at every layer — unit tests with mocked dependencies, slice tests that load only a relevant slice of the application context, and full integration tests. Knowing which test type to use for which scenario avoids the common trap of testing everything with full @SpringBootTest.
Key Points
- @SpringBootTest: loads full application context — use for integration tests; slow, use sparingly
- @WebMvcTest(Controller.class): loads only Spring MVC layer (controllers, filters, exception handlers) — fast, use for controller tests
- @DataJpaTest: loads only JPA layer with in-memory H2 — fast, use for repository tests
- @MockBean: replaces a bean in the application context with a Mockito mock — use in slice tests to isolate the layer
- MockMvc: test MVC controllers without starting a real server — perform(), andExpect(), andDo(print())
- Testcontainers: spin up real Docker containers (Postgres, Redis, Kafka) in tests — gold standard for integration testing
- @Sql / @Sql(executionPhase = BEFORE_TEST_METHOD): run SQL scripts to set up or clean up test data
- @Transactional on test class: each test method runs in a transaction that is rolled back after — no test data pollution
- WireMock: mock external HTTP services — stub responses, verify calls, simulate errors and latency
| Annotation | Loads | Speed | Best for |
|---|---|---|---|
| @SpringBootTest | Full context | Slow | End-to-end integration |
| @WebMvcTest | MVC layer only | Fast | Controller + HTTP mapping |
| @DataJpaTest | JPA + DB only | Fast | Repository queries |
| @JsonTest | Jackson only | Very fast | Serialization/deserialization |
| Plain unit test | Nothing | Fastest | Service / domain logic |
Spring Boot testing: @WebMvcTest + MockMvc, @DataJpaTest + Testcontainers + Postgres, WireMock for external APIs
// Controller test — @WebMvcTest
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired MockMvc mvc;
@MockBean UserService service; // mock the dependency
@Test
void getUser_returnsOk() throws Exception {
given(service.findById(1L))
.willReturn(Optional.of(new UserResponse(1L, "Alice", "a@b.com")));
mvc.perform(get("/api/v1/users/1").accept(APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Alice"));
}
}
// Repository test — real Postgres via Testcontainers
@DataJpaTest
@AutoConfigureTestDatabase(replace = NONE) // don't replace with H2
@Testcontainers
class OrderRepositoryTest {
@Container
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:16").withReuse(true);
@DynamicPropertySource
static void props(DynamicPropertyRegistry r) {
r.add("spring.datasource.url", postgres::getJdbcUrl);
r.add("spring.datasource.username", postgres::getUsername);
r.add("spring.datasource.password", postgres::getPassword);
}
@Autowired OrderRepository repo;
@Test
@Sql("classpath:test-data/orders.sql")
void findByCustomer_returnsOrders() {
var orders = repo.findByCustomer_EmailAndStatus("alice@test.com", PENDING);
assertThat(orders).hasSize(2);
}
}
// WireMock — mock external API
@SpringBootTest(webEnvironment = RANDOM_PORT)
@AutoConfigureWireMock(port = 0) // random port, injected automatically
class PaymentGatewayTest {
@Test
void chargeCard_success() {
stubFor(post(urlEqualTo("/v1/charges"))
.willReturn(okJson("{"status":"succeeded"}")));
var result = paymentClient.charge(100, "card_123");
assertThat(result.status()).isEqualTo("succeeded");
}
}Real-World Example
Testcontainers with withReuse(true) and TESTCONTAINERS_REUSE_ENABLE=true env var keeps the container alive between test runs during local development — container startup only happens once per dev session, making the test suite feel fast even with real databases. In CI, each pipeline run gets a fresh container (no reuse), ensuring isolation.