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.
@MockitoBean PaymentGateway payments; // mock replaces the bean
@MockitoSpyBean AuditLog audit; // spy wraps the real bean
@MockitoBean("beanName") // pick one bean of severalIn 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 mockSpring 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
@MockitoBeanreplaces the bean with a mock that returns empty defaults until you stub it. If no bean of that type exists, one is added.@MockitoSpyBeanwraps 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
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);
}
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;
}
}
class CheckoutController {
private final CheckoutService checkout;
CheckoutController(CheckoutService checkout) {
this.checkout = checkout;
}
("/orders/{ref}/checkout")
ResponseEntity<String> pay( String ref) {
String reference = checkout.checkout(ref);
return reference == null
? ResponseEntity.status(HttpStatus.PAYMENT_REQUIRED).build()
: ResponseEntity.ok(reference);
}
}
class CheckoutIT {
MockMvcTester mvc;
PaymentGateway payments;
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"));
}
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.
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.
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.
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