Unit and Integration Testing
|
This section documents Spring Boot 4.1.x and Spring Framework 7.0.x on the Java 17/21+ baseline, as described by the official Spring Boot reference documentation and each sub-project’s own site (Spring Data, Spring for Apache Kafka, Micrometer, Project Reactor, springdoc-openapi, Spring gRPC, and the others listed in this section’s Bibliography) — which are the references these pages are written and verified against. This content was generated with the assistance of AI and should be verified against those official docs before being relied on in production. Spring Boot and its ecosystem continue to evolve; the examples here target the current 4.1.x / 7.0.x releases. This section’s bibliography lists the reference material consulted while preparing these pages. |
Spring Boot applications are tested at three levels: plain unit tests that isolate a class with mocks, slice tests that load a thin, focused slice of the Spring context, and full integration tests that boot the whole application context — typically against real infrastructure started with Testcontainers.
Unit testing with JUnit 5 and Mockito
Unit tests for a service class should not start any Spring context at all. @ExtendWith(MockitoExtension.class)
initializes Mockito mocks and validates their usage without any Spring infrastructure, keeping the test fast and
focused on a single class’s logic.
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private OrderRepository orderRepository;
@Mock
private PaymentGateway paymentGateway;
@InjectMocks
private OrderService orderService;
@Test
void placeOrder_persistsOrderAndChargesPayment() {
Order order = new Order("ORD-1", BigDecimal.valueOf(49.90));
when(orderRepository.save(any(Order.class))).thenReturn(order);
when(paymentGateway.charge(eq("ORD-1"), any(BigDecimal.class))).thenReturn(true);
Order result = orderService.placeOrder(order);
assertThat(result.getId()).isEqualTo("ORD-1");
verify(orderRepository).save(order);
verify(paymentGateway).charge("ORD-1", BigDecimal.valueOf(49.90));
}
}
@Mock creates a mock for each collaborator; @InjectMocks builds the class under test and injects the mocks
into its constructor (or setters/fields) automatically, so OrderService need not be wired by hand. See the
JUnit 5 User Guide for the full programming model
(@Test, @BeforeEach, @ParameterizedTest, assertions, and extensions) and
Mockito for the mocking API used above.
Argument matchers and argument captors
Argument matchers (any(), eq(), argThat()) let a stub or verification match calls loosely instead of by
exact object equality; mixing matchers and raw values in the same call is not allowed — once one argument uses a
matcher, every argument in that call must.
@Test
void placeOrder_roundsAmountBeforeCharging() {
when(orderRepository.save(any())).thenAnswer(invocation -> invocation.getArgument(0));
orderService.placeOrder(new Order("ORD-2", new BigDecimal("19.995")));
ArgumentCaptor<BigDecimal> amountCaptor = ArgumentCaptor.forClass(BigDecimal.class);
verify(paymentGateway).charge(eq("ORD-2"), amountCaptor.capture());
assertThat(amountCaptor.getValue()).isEqualByComparingTo("20.00");
}
An ArgumentCaptor records the actual value passed to a mocked method so the test can assert on it after the
fact — useful when the value is computed inside the method under test (here, rounding) rather than supplied by
the caller.
Verifying interactions
verify asserts that a mock was called as expected; by default it requires exactly one matching invocation, but
times, never, atLeastOnce, and atMost cover other cardinalities, and verifyNoMoreInteractions catches
unexpected extra calls.
@Test
void placeOrder_doesNotChargeWhenOrderIsInvalid() {
Order invalid = new Order(null, BigDecimal.ZERO);
assertThrows(IllegalArgumentException.class, () -> orderService.placeOrder(invalid));
verify(paymentGateway, never()).charge(anyString(), any());
verifyNoInteractions(orderRepository);
}
Spring Boot test slices
Loading the entire application context for every test is slow and couples unrelated layers together. Spring Boot’s test slice annotations each auto-configure only the beans relevant to one layer and disable the rest, so a test starts quickly and fails only for reasons within that layer. See the Spring Boot testing reference for the complete list of slices and their auto-configuration.
@WebMvcTest: the web layer
@WebMvcTest loads only @Controller/@RestController beans, @ControllerAdvice, converters, and
MVC-related configuration — @Service and @Repository beans are not loaded and must be supplied as mocks.
@WebMvcTest(OrderController.class)
class OrderControllerTest {
@Autowired
private MockMvc mockMvc;
@MockitoBean
private OrderService orderService;
@Test
void findOne_returnsOrderAsJson() throws Exception {
when(orderService.findById(1L)).thenReturn(new OrderDetail(1L, "ORD-1", "SHIPPED"));
mockMvc.perform(get("/api/orders/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("SHIPPED"));
}
}
MockMvc dispatches requests through the real DispatcherServlet machinery (argument resolvers, message
converters, exception handling) without starting an HTTP server, so serialization, validation, and
@ExceptionHandler behavior are all exercised realistically. @MockitoBean (from
org.springframework.test.context.bean.override.mockito) registers a Mockito mock as a bean in the test’s
ApplicationContext, replacing any real bean of that type — it is the current replacement for the older
@MockBean/@SpyBean annotations, which were deprecated in Spring Boot 3.4 and removed in Spring Boot 4.0.
@DataJpaTest: the JPA layer
@DataJpaTest configures an in-memory (or Testcontainers-backed, see below) DataSource, Spring Data JPA
repositories, @Entity scanning, and TestEntityManager — but no @Service, @Controller, or web layer.
Each test method runs inside a transaction that is rolled back afterward by default.
@DataJpaTest
class OrderRepositoryTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private OrderRepository orderRepository;
@Test
void findByStatus_returnsOnlyMatchingOrders() {
entityManager.persist(new OrderEntity("ORD-1", "SHIPPED"));
entityManager.persist(new OrderEntity("ORD-2", "PENDING"));
List<OrderEntity> shipped = orderRepository.findByStatus("SHIPPED");
assertThat(shipped).extracting(OrderEntity::getOrderNumber).containsExactly("ORD-1");
}
}
@DataMongoTest and other data slices
@DataMongoTest is the MongoDB analogue: it configures an embedded or Testcontainers-backed MongoTemplate
and Spring Data MongoDB repositories only. The same pattern extends to @DataRedisTest, @DataNeo4jTest,
@JsonTest (Jackson serialization only), @RestClientTest (a RestClient/RestTemplate and its
MockRestServiceServer), and @WebFluxTest (the reactive analogue of @WebMvcTest) — each loads the minimal
set of beans for that concern.
@DataMongoTest
class OrderDocumentRepositoryTest {
@Autowired
private OrderDocumentRepository orderDocumentRepository;
@Test
void save_assignsGeneratedId() {
OrderDocument saved = orderDocumentRepository.save(new OrderDocument(null, "ORD-3"));
assertThat(saved.getId()).isNotNull();
}
}
Integration testing with @SpringBootTest and Testcontainers
A slice test cannot catch every problem — misconfigured bean wiring across layers, a real query executed
against a real database engine, or a message actually round-tripping through a broker. @SpringBootTest starts
the full application context (optionally with a real embedded HTTP server via webEnvironment), and
Testcontainers supplies real, disposable instances of external dependencies — PostgreSQL, MongoDB, Kafka, Redis — as Docker containers, instead of mocking them or relying on in-memory
substitutes that behave differently from production.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
class OrderIntegrationTest {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private OrderRepository orderRepository;
@Test
void createOrder_persistsToRealDatabase() {
OrderRequest request = new OrderRequest("ORD-9", new BigDecimal("12.50"));
ResponseEntity<OrderDetail> response =
restTemplate.postForEntity("/api/orders", request, OrderDetail.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(orderRepository.findByOrderNumber("ORD-9")).isPresent();
}
}
@ServiceConnection (Spring Boot 3.1+) reads the running container’s connection details and registers a
matching DataSource/ConnectionFactory automatically — no manual @DynamicPropertySource block mapping
JDBC URL, username, and password is needed. The same pattern applies to any container Spring Boot recognizes a
connection detail for:
@SpringBootTest
@Testcontainers
class NotificationIntegrationTest {
@Container
@ServiceConnection
static KafkaContainer kafka = new KafkaContainer(DockerImageName.parse("apache/kafka:3.7.0"));
@Container
@ServiceConnection
static GenericContainer<?> redis = new GenericContainer<>("redis:7-alpine")
.withExposedPorts(6379);
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
@Test
void publishEvent_isConsumedDownstream() {
kafkaTemplate.send("orders.created", "ORD-9");
// ... await consumer side effect, e.g. via Awaitility
}
}
The guiding principle: run one real container per external dependency the code actually talks to, rather
than mocking the database/broker/cache client — this catches SQL dialect issues, index behavior, serialization
formats, and driver quirks that mocks cannot. Containers declared static are shared across all test methods in
the class (started once via the @Testcontainers JUnit 5 extension); a non-static field restarts the
container per test when isolation matters more than speed. See
the Testcontainers for Java documentation for supported container modules,
lifecycle management, and the underlying Testcontainers project that also has
modules for other languages.
Choosing the right level
-
Prefer plain JUnit 5 + Mockito unit tests (no Spring context) for business logic that does not depend on framework wiring — they run in milliseconds.
-
Reach for a test slice (
@WebMvcTest,@DataJpaTest,@DataMongoTest, …) when a test needs Spring’s support for one layer (request mapping, JPA query derivation, JSON serialization) but not the whole application. -
Reserve
@SpringBootTestwith Testcontainers for the smaller set of tests that must prove the application works end to end against real infrastructure — these are slower, so most of the test suite should sit at the first two levels.
Consult the Spring Boot testing reference for the
complete catalog of slices, @MockitoBean/@MockitoSpyBean scoping rules, and how @SpringBootTest selects the
application’s main configuration class.