Testing with Spring

@SpringBootTest and @MockBean, with a complete example testing a service and mocking one of its dependencies.

Why testing a Spring app is different from a plain unit test

A plain JUnit test that does new OrderService(new FakePaymentGateway()) needs no Spring involvement at all — and for testing a single class's logic in isolation, that's still the right, fastest option. Spring's testing support exists for the layer above that: verifying that beans are wired correctly, that configuration loads as expected, and that a service behaves correctly as a Spring-managed bean with its real dependency graph (or a deliberately swapped-out piece of it).

@SpringBootTest: loading a real ApplicationContext

@SpringBootTest starts an actual Spring ApplicationContext for the test — the same container machinery that boots the real application, with every @Component/@Service/@Repository wired exactly as it would be at runtime:

Java
@SpringBootTest
class OrderServiceIntegrationTest {

    @Autowired
    private OrderService orderService;

    @Test
    void placingAnOrderPersistsIt() {
        Order order = orderService.placeOrder(new PlaceOrderRequest("SKU-1", 2));
        assertThat(order.getId()).isNotNull();
    }
}

This is thorough but expensive — every bean in the application gets constructed, which is slow compared to a plain unit test and pulls in real infrastructure (a real, or embedded, database; real configuration) unless something is deliberately swapped out.

Replacing one dependency with a mock: @MockBean

Often a test wants the real OrderService bean, wired by the real container, but with one of its dependencies swapped for a Mockito mock — for example, a PaymentGateway that would otherwise make a real network call:

Java
public interface PaymentGateway {
    PaymentResult charge(String cardToken, BigDecimal amount);
}
Java
@Service
public class OrderService {

    private final PaymentGateway paymentGateway;
    private final OrderRepository orderRepository;

    public OrderService(PaymentGateway paymentGateway, OrderRepository orderRepository) {
        this.paymentGateway = paymentGateway;
        this.orderRepository = orderRepository;
    }

    public Order placeOrder(PlaceOrderRequest request) {
        PaymentResult result = paymentGateway.charge(request.cardToken(), request.amount());
        if (!result.successful()) {
            throw new PaymentFailedException(result.reason());
        }
        return orderRepository.save(new Order(request.sku(), request.quantity(), result.transactionId()));
    }
}
Java
@SpringBootTest
class OrderServiceTest {

    @Autowired
    private OrderService orderService;

    @MockBean
    private PaymentGateway paymentGateway; // replaces the real bean in the context with a Mockito mock

    @Test
    void successfulPaymentSavesTheOrder() {
        when(paymentGateway.charge(anyString(), any(BigDecimal.class)))
            .thenReturn(new PaymentResult(true, "txn-123", null));

        Order order = orderService.placeOrder(new PlaceOrderRequest("SKU-1", 2, "tok-1", BigDecimal.TEN));

        assertThat(order.getTransactionId()).isEqualTo("txn-123");
    }

    @Test
    void failedPaymentThrowsAndNeverPersists() {
        when(paymentGateway.charge(anyString(), any(BigDecimal.class)))
            .thenReturn(new PaymentResult(false, null, "card_declined"));

        assertThrows(PaymentFailedException.class, () ->
            orderService.placeOrder(new PlaceOrderRequest("SKU-1", 2, "tok-1", BigDecimal.TEN))
        );
    }
}

@MockBean does two things: it registers a Mockito mock as a bean in the test's ApplicationContext, and it replaces whatever real bean of that type would otherwise have been wired — everywhere the container injects PaymentGateway, it now injects this mock instead, including into the real OrderService bean under test.

A note on @MockitoBean

Starting with Spring Boot 3.4, @MockBean is deprecated in favor of @MockitoBean (org.springframework.test.bean.override.mockito.MockitoBean), part of a more general "bean override" mechanism for tests. It's used exactly the same way (@MockitoBean private PaymentGateway paymentGateway;) — the rename exists because the new mechanism generalizes beyond just Mockito mocks. Existing @MockBean code still compiles and works on 3.4+, but new code should prefer @MockitoBean.

Where this fits with slice tests

@SpringBootTest loads the entire application context, which is correct for a genuine end-to-end test but overkill for testing just a web layer or just a repository. Spring Boot's narrower "slice" test annotations (@WebMvcTest, @DataJpaTest) load only the beans relevant to one layer, dramatically reducing startup cost — covered in depth on the Spring Boot track's own testing page, since they're Boot-specific rather than core-Spring features.

Common mistakes

  • Reaching for @SpringBootTest for every test by default — it's the slowest and heaviest option; a plain unit test with hand-constructed dependencies (no Spring at all) is almost always faster and just as effective for testing one class's logic in isolation.
  • Forgetting that @MockBean replaces the bean for every test in the class (and resets it between tests) — stubbing behavior in one @Test method that another test in the same class doesn't expect can cause confusing cross-test interference if mocks aren't re-stubbed per test.
  • Mocking a dependency that the test doesn't actually need to control, when the real bean (an in-memory implementation, or a genuinely fast dependency) would exercise more real behavior for the same effort.
  • Using @Autowired field injection heavily in test classes without a second thought — acceptable in tests (there's no constructor a test framework calls for you), unlike production code where constructor injection is strongly preferred.