Mocking Beans in Spring Boot Tests with @MockitoBean
Introduction
This tutorial explains how to replace a Spring bean with a mock in a test, using Spring Framework's@MockitoBean and @MockitoSpyBean. We will see what a mock is and when to use one, and test an order service whose payment client must never call the real payment provider. First with plain Mockito and no Spring at all, then inside a Spring context, with stubbing, exceptions, verification, an argument captor and a spy. We will also see that @MockBean from older tutorials no longer exists in Spring Boot 4, and how Mockito's strict stubs report a stub that the code never uses.What a mock is
A mock is a stand-in object that implements the same type as the real one, but does only what the test tells it. Asked a question it was not told about, it returns a default:null, 0, false or an empty collection. It also records every call, so that the test can check afterwards how it was used. Mocks are the right tool for dependencies that are slow, unreliable, expensive or have side effects: a payment provider, an email server, another team's service. They are the wrong tool for simple, fast code: a real object, as the stock check here, tests more and costs nothing.Mockito, part of Spring Boot's test starter, creates mocks, and offers two styles for stubbing them. The BDD style used here reads as given-when-then:
given(payments.charge("Ann", 900)).willReturn("PAY-42"). The classic style, when(...).thenReturn(...), does the same.
The application
An order service checks the stock, charges the customer through a payment client, and sends a confirmation. The payment client stands for a call to a real payment provider; here it simply throws, so that a test that accidentally uses it fails at once -src/main/java/.../Order.java
//Order.java
package com.javabrahman.testing.mocks;
public record Order(String customer, String isbn, int quantity, int amount) {
}
src/main/java/.../PaymentClient.java - calls an external payment provider
//PaymentClient.java
package com.javabrahman.testing.mocks;
import org.springframework.stereotype.Component;
@Component
public class PaymentClient {
// In a real application this sends an HTTP request to a payment provider - not something a test should do
public String charge(String customer, int amount) {
throw new IllegalStateException("no network access in this example: the real provider is not reachable");
}
}
src/main/java/.../Stock.java
//Stock.java
package com.javabrahman.testing.mocks;
import java.util.Map;
import org.springframework.stereotype.Component;
@Component
public class Stock {
private final Map<String, Integer> copies = Map.of("978-0134685991", 5, "978-0132350884", 0);
public boolean available(String isbn, int quantity) {
return copies.getOrDefault(isbn, 0) >= quantity;
}
}
src/main/java/.../Notifier.java
//Notifier.java
package com.javabrahman.testing.mocks;
import org.springframework.stereotype.Component;
@Component
public class Notifier {
public String confirmation(Order order, String paymentId) {
return "Dear " + order.customer() + ", order paid (" + paymentId + ")";
}
public void send(String customer, String message) {
System.out.println(" [email to " + customer + "] " + message);
}
}
src/main/java/.../OrderService.java - the class under test
//OrderService.java
package com.javabrahman.testing.mocks;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private final Stock stock;
private final PaymentClient payments;
private final Notifier notifier;
public OrderService(Stock stock, PaymentClient payments, Notifier notifier) {
this.stock = stock;
this.payments = payments;
this.notifier = notifier;
}
public String place(Order order) {
if (!stock.available(order.isbn(), order.quantity())) {
return "REJECTED: out of stock";
}
String paymentId;
try {
paymentId = payments.charge(order.customer(), order.amount());
} catch (RuntimeException e) {
return "FAILED: " + e.getMessage();
}
notifier.send(order.customer(), notifier.confirmation(order, paymentId));
return "PLACED: " + paymentId;
}
}
src/main/java/.../ShopApp.java
//ShopApp.java
package com.javabrahman.testing.mocks;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ShopApp {
public static void main(String[] args) {
SpringApplication.run(ShopApp.class, args);
}
}
Mocks without Spring
The order service is an ordinary class with three constructor parameters, so a unit test needs no Spring at all.MockitoExtension creates a mock for each @Mock field, and @InjectMocks calls the service's constructor with them -
src/test/java/.../OrderServiceUnitTest.java - plain Mockito, no Spring
//OrderServiceUnitTest.java
package com.javabrahman.testing.mocks;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class OrderServiceUnitTest {
@Mock Stock stock;
@Mock PaymentClient payments;
@Mock Notifier notifier;
@InjectMocks OrderService service; // created with the three mocks above
@Test
void placesAnOrder() {
given(stock.available("978-0134685991", 2)).willReturn(true);
given(payments.charge("Ann", 900)).willReturn("PAY-1");
given(notifier.confirmation(any(), anyString())).willReturn("thanks");
assertThat(service.place(new Order("Ann", "978-0134685991", 2, 900))).isEqualTo("PLACED: PAY-1");
verify(notifier).send("Ann", "thanks");
}
@Test
void neverChargesWhenOutOfStock() {
given(stock.available(anyString(), anyInt())).willReturn(false);
assertThat(service.place(new Order("Raj", "978-0132350884", 1, 380))).startsWith("REJECTED");
verifyNoInteractions(payments, notifier);
}
}
Mocks inside a Spring context: @MockitoBean
When a test needs Spring - an integration test, or a slice such as the@WebMvcTest of the previous tutorialClick to read tutorial: Spring Boot Slice Tests: @WebMvcTest, @DataJpaTest and More - the mock must be in the application context, in place of the real bean, so that the beans that depend on it receive the mock -
@MockitoBeanon a test field replaces the bean of that type with a Mockito mock, or adds one if there is none, and injects it into the field.@MockitoSpyBeanwraps the existing bean in a spy: the real methods run, and every call is recorded forverify(). Individual methods can still be stubbed.
org.springframework.test.context.bean.override.mockito. They replace Spring Boot's @MockBean and @SpyBean, which were deprecated in Spring Boot 3.4 and are gone in Spring Boot 4: code using them no longer compiles, and the fix is a change of annotation and import.
src/test/java/.../OrderServiceSpringTest.java - @MockitoBean and @MockitoSpyBean
//OrderServiceSpringTest.java
package com.javabrahman.testing.mocks;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.context.bean.override.mockito.MockitoSpyBean;
@SpringBootTest
class OrderServiceSpringTest {
@Autowired
OrderService service; // the real service, from the context
@MockitoBean
PaymentClient payments; // replaces the PaymentClient bean with a mock
@MockitoSpyBean
Notifier notifier; // wraps the real Notifier bean: real behaviour, but recorded
@Test
void chargesAndNotifies() {
given(payments.charge("Ann", 900)).willReturn("PAY-42");
assertThat(service.place(new Order("Ann", "978-0134685991", 2, 900))).isEqualTo("PLACED: PAY-42");
ArgumentCaptor<String> message = ArgumentCaptor.forClass(String.class);
verify(notifier).send(anyString(), message.capture());
System.out.println(" captured message: " + message.getValue());
assertThat(message.getValue()).contains("PAY-42");
}
@Test
void reportsAFailedPayment() {
willThrow(new IllegalStateException("card declined")).given(payments).charge(anyString(), anyInt());
assertThat(service.place(new Order("Raj", "978-0134685991", 1, 450))).isEqualTo("FAILED: card declined");
verify(notifier, never()).send(anyString(), anyString());
}
@Test
void eachTestGetsAFreshMock() {
// no stubbing here: an unstubbed mock returns null, and the stubbing of the other tests is gone
assertThat(payments.charge("Ann", 900)).isNull();
verify(payments, times(1)).charge("Ann", 900);
}
@Test
void theRealStockIsUsed() {
assertThat(service.place(new Order("Eve", "978-0132350884", 1, 380))).startsWith("REJECTED");
verify(payments, never()).charge(anyString(), anyInt());
}
}
[email to Ann] Dear Ann, order paid (PAY-42)
captured message: Dear Ann, order paid (PAY-42)
╷
└─ JUnit Jupiter ✔
├─ OrderServiceUnitTest ✔
│ ├─ placesAnOrder() ✔
│ └─ neverChargesWhenOutOfStock() ✔
└─ OrderServiceSpringTest ✔
├─ reportsAFailedPayment() ✔
├─ chargesAndNotifies() ✔
├─ eachTestGetsAFreshMock() ✔
└─ theRealStockIsUsed() ✔
[ 3 containers found ]
[ 0 containers skipped ]
[ 3 containers started ]
[ 0 containers aborted ]
[ 3 containers successful ]
[ 0 containers failed ]
[ 6 tests found ]
[ 0 tests skipped ]
[ 6 tests started ]
[ 0 tests aborted ]
[ 6 tests successful ]
[ 0 tests failed ]
Explanation of the output
- Plain Mockito:
placesAnOrder()stubbed all three dependencies, andverify(notifier).send("Ann", "thanks")checked that the confirmation went out.neverChargesWhenOutOfStock()stubbed the stock check with the matchersanyString()andanyInt(), andverifyNoInteractions(payments, notifier)proved that a rejected order charges nobody. - A spy really runs: the line "[email to Ann] Dear Ann, order paid (PAY-42)" was printed by the real
Notifier, because@MockitoSpyBeankeeps the real behaviour. TheArgumentCaptorcaught the message passed tosend(), so the test could print it and check that it contains the payment id. - An exception from a mock:
willThrow(...).given(payments).charge(...)made the payment fail, the service answered "FAILED: card declined", andverify(notifier, never()).send(...)confirmed that no email was sent. For methods returningvoid, this form - or the classicdoThrow().when()- is needed, sincegiven(...)cannot take avoidcall. - Mocks are reset: in
eachTestGetsAFreshMock(), the mock had no stubbing - the"PAY-42"of another test was gone - so it returnednull, andtimes(1)counted only this test's call. Spring resets@MockitoBeanmocks after every test. - Real and mock beans together:
theRealStockIsUsed()ran against the realStockbean, which has no copies of "Clean Code", and the mock proved that the payment provider was never called.
Strict stubs: a stub nobody uses
MockitoExtension uses strict stubs by default: a stubbing that the test never uses makes the test fail. That sounds harsh, but an unused stub usually means the test does not test what its author thought, or is left over from older code -
src/test/java/.../UnusedStubbingTest.java - a stub the code never uses
//UnusedStubbingTest.java
package com.javabrahman.testing.mocks;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class UnusedStubbingTest {
@Mock Stock stock;
@Mock PaymentClient payments;
@Mock Notifier notifier;
@InjectMocks OrderService service;
@Test
void outOfStock() {
given(stock.available("978-0132350884", 1)).willReturn(false);
given(payments.charge("Raj", 380)).willReturn("PAY-7"); // never used: the order is rejected first
assertThat(service.place(new Order("Raj", "978-0132350884", 1, 380))).startsWith("REJECTED");
}
}
└─ UnusedStubbingTest ✔
└─ outOfStock() ✘
Unnecessary stubbings detected.
1. -> at com.javabrahman.testing.mocks.UnusedStubbingTest.outOfStock(UnusedStubbingTest.java:24)
JUnit Jupiter:UnusedStubbingTest:outOfStock()
Unnecessary stubbings detected.
1. -> at com.javabrahman.testing.mocks.UnusedStubbingTest.outOfStock(UnusedStubbingTest.java:24)
[ 0 tests successful ]
[ 1 tests failed ]
payments.charge. The order never reached the payment, so that stub was dead code. The fix is to delete it, which also leaves the test describing exactly what the code does. Where a stub is used only by some of the tests of a shared setup, lenient().when(...) marks it as allowed to stay unused.
Good practice with mocks
- Mock what you do not own and cannot run: external services, clocks, random numbers. Do not mock simple value objects or your own fast helpers.
- Prefer plain Mockito for unit tests of a single class: it needs no context and runs in milliseconds. Use
@MockitoBeanwhen the test needs Spring around the mock. - Verify what matters: a payment that must not happen, an email that must be sent. Verifying every call couples the test to the implementation, and it breaks with each refactoring.
- Mind the context cache: every distinct combination of
@MockitoBeanfields is a different context. Many test classes with slightly different mocks each start their own context, a common reason for slow test suites, covered in testing best practicesClick to read tutorial: Spring Boot Testing Best Practices and Faster Test Suites. - Mockito as a Java agent: on recent JDKs, Mockito should be loaded as a Java agent instead of attaching itself at runtime, or the JDK prints a warning, and will refuse it in a future release. The Maven Surefire and Gradle setups in Spring Boot projects can pass
-javaagentwith the Mockito jar to the test JVM; the tests in this series run that way.
Summary
In this tutorial we replaced dependencies with mocks. With plain Mockito,@Mock and @InjectMocks tested the order service without Spring, stubbing with given().willReturn() and checking with verify() and verifyNoInteractions(). Inside a Spring context, @MockitoBean replaced the payment client bean, and @MockitoSpyBean wrapped the real notifier, whose email was really printed and whose message an ArgumentCaptor caught. A thrown exception, never(), and the reset of mocks between tests completed the picture, and Mockito's strict stubs failed a test because of a stub the code never used. @MockBean and @SpyBean are gone in Spring Boot 4; these annotations replace them. The next tutorial tests REST APIs in depth: MockMvc and MockMvcTesterClick to read tutorial: Testing REST APIs with MockMvc and MockMvcTester.Spring Testing Tutorials
Spring Boot Slice Tests: @WebMvcTest, @DataJpaTest and MoreClick to read tutorial: Spring Boot Slice Tests: @WebMvcTest, @DataJpaTest and More Testing REST APIs with MockMvc and MockMvcTesterClick to read tutorial: Testing REST APIs with MockMvc and MockMvcTester Testing in Spring Boot: The Test Pyramid and @SpringBootTestClick to read tutorial: Testing in Spring Boot: The Test Pyramid and @SpringBootTest Testing Spring Security: @WithMockUser, CSRF and JWTClick to read tutorial: Testing Spring Security: @WithMockUser, CSRF and JWT Contract Testing with Spring Cloud ContractClick to read tutorial: Contract Testing with Spring Cloud Contract Spring Boot Testing Best Practices and Faster Test SuitesClick to read tutorial: Spring Boot Testing Best Practices and Faster Test Suites
Spring Boot Slice Tests: @WebMvcTest, @DataJpaTest and MoreClick to read tutorial: Spring Boot Slice Tests: @WebMvcTest, @DataJpaTest and More Testing REST APIs with MockMvc and MockMvcTesterClick to read tutorial: Testing REST APIs with MockMvc and MockMvcTester Testing in Spring Boot: The Test Pyramid and @SpringBootTestClick to read tutorial: Testing in Spring Boot: The Test Pyramid and @SpringBootTest Testing Spring Security: @WithMockUser, CSRF and JWTClick to read tutorial: Testing Spring Security: @WithMockUser, CSRF and JWT Contract Testing with Spring Cloud ContractClick to read tutorial: Contract Testing with Spring Cloud Contract Spring Boot Testing Best Practices and Faster Test SuitesClick to read tutorial: Spring Boot Testing Best Practices and Faster Test Suites