Mocking Beans with @MockitoBean

Replace a bean in the test's application context with a Mockito mock or spy, and keep the number of distinct contexts under control.

Syntax@MockitoBean PaymentGateway payments; // mock replaces the bean @MockitoSpyBean AuditLog audit; // spy wraps the real bean @MockitoBean("beanName") // pick one bean of several

In a Spring test the class under test gets its collaborators from the application context, not from your hands. To control one of them, replace the bean itself. @MockitoBean (from org.springframework.test.context.bean.override.mockito, part of Spring Framework) does that: Spring creates a Mockito mock, puts it in the context in place of the real bean, and injects it into your test field.

@MockitoBean
PaymentGateway payments;    // every bean that needs a PaymentGateway gets this mock

Spring Boot's own @MockBean and @SpyBean are not in Spring Boot 4. Replace them with @MockitoBean and @MockitoSpyBean; the usage is the same.

Mock or spy

  • @MockitoBean replaces the bean with a mock that returns empty defaults until you stub it. If no bean of that type exists, one is added.
  • @MockitoSpyBean wraps the real bean. Calls go to the real code, and you can verify them or stub single methods. A bean of that type must already exist.

Mocks are reset after each test method by default, so stubbing in one test does not leak into the next. Fields must not be final. When several beans share a type, name the one to replace: @MockitoBean("stripeGateway").

What to mock

Mock the edges of the system: payment providers, email senders, HTTP clients for other services. These are slow, cost money or have side effects you cannot undo. Do not mock your own repositories in a @SpringBootTest; use a real database in a container, otherwise the test checks less than a unit test would while running far slower.

The hidden cost

The set of overridden beans is part of the context cache key. Class A mocking PaymentGateway and class B mocking PaymentGateway plus EmailSender need two different contexts, each started from scratch. In a large suite this is often the main cause of slow builds. Declare the common mocks once: in a shared base class, or with the class-level form @MockitoBean(types = {PaymentGateway.class, EmailSender.class}) on a base class or a custom annotation.

For a replacement that is not a mock, such as a fixed Clock, @TestBean swaps in the result of a static factory method.

Example

Example · java
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;

import java.math.BigDecimal;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

record ChargeResult(boolean approved, String reference) {
}

interface PaymentGateway {
    ChargeResult charge(String orderRef, BigDecimal amount);
}

@Service
class CheckoutService {

    private final PaymentGateway payments;

    CheckoutService(PaymentGateway payments) {
        this.payments = payments;
    }

    String checkout(String orderRef) {
        ChargeResult result = payments.charge(orderRef, new BigDecimal("30.00"));
        return result.approved() ? result.reference() : null;
    }
}

@RestController
class CheckoutController {

    private final CheckoutService checkout;

    CheckoutController(CheckoutService checkout) {
        this.checkout = checkout;
    }

    @PostMapping("/orders/{ref}/checkout")
    ResponseEntity<String> pay(@PathVariable String ref) {
        String reference = checkout.checkout(ref);
        return reference == null
                ? ResponseEntity.status(HttpStatus.PAYMENT_REQUIRED).build()
                : ResponseEntity.ok(reference);
    }
}

@SpringBootTest
@AutoConfigureMockMvc
class CheckoutIT {

    @Autowired
    MockMvcTester mvc;

    @MockitoBean
    PaymentGateway payments;

    @Test
    void returnsThePaymentReferenceWhenApproved() {
        given(payments.charge(eq("ord-1"), any())).willReturn(new ChargeResult(true, "pay_123"));

        assertThat(mvc.post().uri("/orders/{ref}/checkout", "ord-1"))
                .hasStatusOk()
                .hasBodyTextEqualTo("pay_123");
        then(payments).should().charge("ord-1", new BigDecimal("30.00"));
    }

    @Test
    void answersPaymentRequiredWhenDeclined() {
        given(payments.charge(eq("ord-2"), any())).willReturn(new ChargeResult(false, null));

        assertThat(mvc.post().uri("/orders/{ref}/checkout", "ord-2"))
                .hasStatus(HttpStatus.PAYMENT_REQUIRED);
    }
}

When to use it

  • Replace a card payment client in a full application test so no real charge is ever attempted from CI.
  • Verify that every approved refund writes an audit entry, using a spy on the real audit bean.
  • Halve a suite's run time by moving scattered @MockitoBean declarations into one shared base class.

More examples

Spying on a real bean

A spy keeps the real behaviour, so <code>entries()</code> returns what was actually recorded, while Mockito still lets you verify the call. Use spies sparingly: they are useful when the real bean is cheap and correct and you only need to observe it. If you find yourself stubbing most of a spy's methods, a plain mock is clearer.

Example · java
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.never;

import java.util.ArrayList;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;

@Component
class AuditLog {

    private final List<String> entries = new ArrayList<>();

    void record(String entry) {
        entries.add(entry);
    }

    List<String> entries() {
        return List.copyOf(entries);
    }
}

@Service
class RefundService {

    private final AuditLog audit;

    RefundService(AuditLog audit) {
        this.audit = audit;
    }

    boolean refund(String orderRef, boolean withinPolicy) {
        if (!withinPolicy) {
            return false;
        }
        audit.record("refund " + orderRef);
        return true;
    }
}

@SpringBootTest
class RefundServiceIT {

    @Autowired
    RefundService refunds;

    @MockitoSpyBean
    AuditLog audit;

    @Test
    void auditsEveryRefund() {
        refunds.refund("ord-7", true);

        then(audit).should().record("refund ord-7");
        assertThat(audit.entries()).contains("refund ord-7");
    }

    @Test
    void doesNotAuditARejectedRefund() {
        refunds.refund("ord-8", false);

        then(audit).should(never()).record("refund ord-8");
    }
}

Declaring shared mocks once on a base class

<code>@MockitoBean</code> on a type with <code>types</code> registers one mock per listed type, and subclasses inherit it. Both subclasses have the same overrides, so they share one cached context. The mocks are ordinary beans, so you can <code>@Autowired</code> them to stub and verify, exactly as with a field-level mock.

Example · java
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.assertj.MockMvcTester;

interface PaymentGateway {
    boolean charge(String orderRef);
}

interface EmailSender {
    void send(String to, String subject);
}

@SpringBootTest
@AutoConfigureMockMvc
@MockitoBean(types = {PaymentGateway.class, EmailSender.class})
abstract class ExternalServicesMockedIT {

    @Autowired
    protected MockMvcTester mvc;

    @Autowired
    protected PaymentGateway payments;

    @Autowired
    protected EmailSender emails;
}

class CheckoutFlowIT extends ExternalServicesMockedIT {

    @Test
    void chargesThroughTheMockedGateway() {
        given(payments.charge("ord-1")).willReturn(true);

        assertThat(payments.charge("ord-1")).isTrue();
    }
}

class SignupFlowIT extends ExternalServicesMockedIT {

    @Test
    void startsWithTheSameContext() {
        assertThat(mvc).isNotNull();
    }
}

Choosing one of several beans of the same type

When two beans share a type, a mock by type alone is ambiguous, so the bean name picks the one to replace. Here both notifiers are mocked so the fallback rule can be tested: the push mock reports failure and the SMS mock records the call. A mock that has not been stubbed returns <code>false</code> for a <code>boolean</code>, which is worth remembering when a test behaves unexpectedly.

Example · java
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Service;
import org.springframework.test.context.bean.override.mockito.MockitoBean;

interface Notifier {
    boolean notify(String userId, String message);
}

@Configuration
class NotifierConfig {

    @Bean
    Notifier smsNotifier() {
        return (userId, message) -> true;
    }

    @Bean
    Notifier pushNotifier() {
        return (userId, message) -> true;
    }
}

@Service
class DeliveryAlerts {

    private final Notifier sms;
    private final Notifier push;

    DeliveryAlerts(@Qualifier("smsNotifier") Notifier sms, @Qualifier("pushNotifier") Notifier push) {
        this.sms = sms;
        this.push = push;
    }

    void outForDelivery(String userId) {
        if (!push.notify(userId, "Your order is out for delivery")) {
            sms.notify(userId, "Your order is out for delivery");
        }
    }
}

@SpringBootTest
class DeliveryAlertsIT {

    @Autowired
    DeliveryAlerts alerts;

    @MockitoBean("pushNotifier")
    Notifier push;

    @MockitoBean("smsNotifier")
    Notifier sms;

    @Test
    void fallsBackToSmsWhenPushFails() {
        given(push.notify("u1", "Your order is out for delivery")).willReturn(false);

        alerts.outForDelivery("u1");

        then(sms).should().notify("u1", "Your order is out for delivery");
    }
}

Discussion

  • Be the first to comment on this lesson.