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 @SpringBootTest with 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.