Here is the English translation of the article body.
You’ve written your first Service-layer test with @Mock and @InjectMocks. But are you getting stuck on the very next step? Trying to stub a void method and hitting a compile error, not knowing how to make a mock throw an exception, wanting to check the contents of a saved entity but having no way to do it, and then getting scolded by a mysterious UnnecessaryStubbingException. I used to search for every one of these myself at first.
In this article, we’ll set up a single OrderService that depends on three things: a Repository, an external payment API, and event publishing. Then we’ll build up its test suite step by step. Along the way, we’ll deliberately run into InvalidUseOfMatchersException and UnnecessaryStubbingException, and cover how to read the messages and fix them.
The basics of JUnit 5 and the fundamentals of when/verify are covered in the introductory article, so this article focuses on what comes after. The verified environment is Spring Boot 3.5.x / JUnit 5 / Mockito 5.x (bundled with spring-boot-starter-test).
The OrderService and Its Dependencies
All the tests that follow are written against this OrderService. placeOrder proceeds in the order of amount calculation, payment, save, and event publishing. cancelOrder fetches the order, issues a refund, updates the status, and saves.
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentClient paymentClient;
private final ApplicationEventPublisher eventPublisher;
public OrderService(OrderRepository orderRepository,
PaymentClient paymentClient,
ApplicationEventPublisher eventPublisher) {
this.orderRepository = orderRepository;
this.paymentClient = paymentClient;
this.eventPublisher = eventPublisher;
}
public Order placeOrder(OrderRequest request) {
int total = request.unitPrice() * request.quantity();
PaymentResult result = paymentClient.charge(total, "JPY");
Order order = new Order(null, request.customerId(), total,
result.transactionId(), OrderStatus.PAID);
Order saved = orderRepository.save(order);
eventPublisher.publishEvent(new OrderPlacedEvent(saved.id(), total));
return saved;
}
public Order cancelOrder(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
paymentClient.refund(order.transactionId()); // voidメソッド
return orderRepository.save(order.withStatus(OrderStatus.CANCELED));
}
}
The surrounding classes are kept to a minimum. In a real application, Order would probably be a JPA entity, but since we want to focus on how to use Mockito, the article simplifies it as a record. Note that both exceptions extend RuntimeException. Since charge() and refund() do not declare throws, only unchecked exceptions can be thrown via the thenThrow / doThrow described later. Passing a checked exception fails with a MockitoException (Checked exception is invalid for this method!).
public record Order(Long id, String customerId, int totalAmount,
String transactionId, OrderStatus status) {
public Order withStatus(OrderStatus next) {
return new Order(id, customerId, totalAmount, transactionId, next);
}
}
public enum OrderStatus { PAID, CANCELED }
public record OrderRequest(String customerId, int unitPrice, int quantity) {}
public record PaymentResult(String transactionId) {}
public record OrderPlacedEvent(Long orderId, int totalAmount) {}
public interface PaymentClient {
PaymentResult charge(int amount, String currency);
void refund(String transactionId);
}
public interface OrderRepository extends CrudRepository<Order, Long> {}
public class PaymentException extends RuntimeException {
public PaymentException(String message) { super(message); }
}
public class OrderNotFoundException extends RuntimeException {
public OrderNotFoundException(Long orderId) { super("order not found: " + orderId); }
}
Setting Up Tests Without a Spring Context
You don’t need @SpringBootTest for Service-layer unit tests. Annotate the class with @ExtendWith(MockitoExtension.class), declare the dependencies with @Mock, and assemble the OrderService with @InjectMocks. Startup finishes in tens of milliseconds.
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock OrderRepository orderRepository;
@Mock PaymentClient paymentClient;
@Mock ApplicationEventPublisher eventPublisher;
@InjectMocks OrderService orderService;
OrderRequest request;
@BeforeEach
void setUp() {
request = new OrderRequest("user-1", 500, 2); // 合計1,000円
}
@Test
void placeOrder_決済成功ならPAIDの注文を返す() {
// 次の節でスタブを埋めていきます
}
}
@InjectMocks first attempts injection via the constructor. Since OrderService uses constructor injection, the three mocks are resolved by type and passed in directly. With a field-injected Service, the injection behavior becomes harder to follow, so constructor injection is recommended for testability as well. For details, see the comparison of the three DI styles.
Note that @InjectMocks does not throw an exception if there is a dependency it cannot inject. It simply proceeds with that field left as null. If you get a NullPointerException during a test, first suspect a missing mock declaration.
Keep @BeforeEach limited to creating the shared OrderRequest, and put stubs inside each test method. This policy works well with Strict Stubs, described later.
Stubbing Return Values with when/thenReturn
Let’s complete the happy path first. Stub charge() to return a successful payment, and save() to return the Order it was given as-is.
@Test
void placeOrder_決済成功ならPAIDの注文を返す() {
when(paymentClient.charge(1000, "JPY"))
.thenReturn(new PaymentResult("tx-123"));
when(orderRepository.save(any(Order.class)))
.thenAnswer(inv -> inv.getArgument(0)); // 渡されたOrderをそのまま返す
Order order = orderService.placeOrder(request);
assertThat(order.status()).isEqualTo(OrderStatus.PAID);
assertThat(order.totalAmount()).isEqualTo(1000);
assertThat(order.transactionId()).isEqualTo("tx-123");
}
thenReturn returns a fixed value, while thenAnswer builds the return value using the arguments at call time. For “what goes in comes back out” behavior like save(), thenAnswer is convenient.
Chaining thenReturn returns different values in sequence for each call. Since OrderService has no retry logic, here we just call the mock directly to show the behavior. Remember this as the tool for situations like “empty on the first call, a value on the second.”
Order paidOrder = new Order(1L, "user-1", 1000, "tx-123", OrderStatus.PAID);
when(orderRepository.findById(1L))
.thenReturn(Optional.empty())
.thenReturn(Optional.of(paidOrder));
// デモとしてモックを直接呼んでいます
assertThat(orderRepository.findById(1L)).isEmpty(); // 1回目
assertThat(orderRepository.findById(1L)).isPresent(); // 2回目以降はずっと最後の値
You can also write it with varargs as thenReturn(a, b), but with generic types like Optional this produces an unchecked warning (unchecked generic array creation), so the chained form is cleaner.
When you want to throw an exception, use thenThrow. Reproduce a payment failure and confirm that placeOrder propagates the exception as-is.
@Test
void placeOrder_決済失敗なら例外を伝播する() {
when(paymentClient.charge(anyInt(), anyString()))
.thenThrow(new PaymentException("card declined"));
assertThrows(PaymentException.class, () -> orderService.placeOrder(request));
}
To summarize the roles in one line: when and the do family described later are the Given setup that says “this mock behaves this way,” while verify is the Then that checks “was it called.” Just keeping these two apart makes tests considerably more readable.
Argument Matchers any/eq/argThat and InvalidUseOfMatchersException
The anyInt() and any(Order.class) from earlier are argument matchers. The three common patterns are “only the type needs to match,” “fix only some arguments,” and “conditional.” The pitfall is mixing matchers with raw values. You want to write “any amount, but the currency must be JPY,” and you forget to add eq().
// 書き方の対比用にまとめています。同じ呼び出しを複数回スタブすると後勝ちで上書きされ、
// Strict Stubsでは使われなかった分が UnnecessaryStubbingException になるので、
// 1つのテストにこのまま並べないでください。
// 型だけ合えばよい
when(orderRepository.findById(anyLong())).thenReturn(Optional.of(paidOrder));
// 一部だけ固定したい(他の引数もマッチャで揃える)
when(paymentClient.charge(anyInt(), eq("JPY")))
.thenReturn(new PaymentResult("tx-123"));
// 条件付き。プリミティブ引数は intThat / longThat などを使う
when(paymentClient.charge(intThat(amount -> amount > 0), anyString()))
.thenReturn(new PaymentResult("tx-123"));
when(orderRepository.save(argThat(o -> o.status() == OrderStatus.PAID)))
.thenAnswer(inv -> inv.getArgument(0));
// NG: 第1引数はマッチャ、第2引数は実値
when(paymentClient.charge(anyInt(), "JPY"))
.thenReturn(new PaymentResult("tx-123"));
// 実行すると、この行で次の例外が出る
// org.mockito.exceptions.misusing.InvalidUseOfMatchersException:
// Invalid use of argument matchers!
// 2 matchers expected, 1 recorded:
// -> at OrderServiceTest.placeOrder_...(OrderServiceTest.java:42)
//
// This exception may occur if matchers are combined with raw values:
// //incorrect:
// someMethod(any(), "raw String");
// When using matchers, all arguments have to be provided by matchers.
// For example:
// //correct:
// someMethod(any(), eq("String by matcher"));
The rule is: “if you use even one matcher, make every argument a matcher.” Raw values can simply be wrapped with eq("JPY"), so the NG example above passes once you change it to charge(anyInt(), eq("JPY")).
There is a caveat around null. Type-specified any such as any(Order.class) does not match null since Mockito 2.1.0. The same goes for anyInt() and anyString(). On the other hand, the argument-less any() matches any value including null. If you intentionally want to stub a call that passes null, making it explicit with isNull() also communicates the intent to readers.
Read the situations where this exception occurs as two separate cases. When you mix raw values and matchers as above, the matcher count is checked against the argument count at the moment the mock is called, so it fails immediately on that line. On the other hand, if you call any() or similar outside of when or verify (for example, passing it as a method argument to a real object), the matcher remains recorded on the thread. If you’re using MockitoExtension, validateMockitoUsage() runs at the end of the test and reports it as “Misplaced or misused argument matcher detected here” at the end of the same test. If you’re initializing with only MockitoAnnotations.openMocks(this) and using neither the extension nor the runner, it may be reported in a different test that next touches a mock. If you see a method name you don’t recognize, suspect the preceding test.
Stubbing void Methods with doThrow/doNothing/doAnswer
refund() and publishEvent() are void. Writing when(paymentClient.refund("tx-123")) results in a compile error because a void call cannot be passed to when() as an expression. Void methods are written in the order do〜().when(mock).method().
As an error case for cancelOrder, let’s verify that if the refund fails, the status is not updated and saved.
@Test
void cancelOrder_返金失敗ならステータスを更新しない() {
Order paid = new Order(1L, "user-1", 1000, "tx-123", OrderStatus.PAID);
when(orderRepository.findById(1L)).thenReturn(Optional.of(paid));
doThrow(new PaymentException("refund failed"))
.when(paymentClient).refund("tx-123");
assertThrows(PaymentException.class, () -> orderService.cancelOrder(1L));
verify(orderRepository, never()).save(any());
}
doNothing() is the same as a mock’s default behavior, so there is almost no point in writing it on its own. It’s used in cases like doThrow(...).doNothing().when(...), where you want only the first call to fail and return to normal behavior from the second call onward.
When you want to reproduce a side effect, use doAnswer. You can collect the events passed to publishEvent() in a List and check their contents afterward.
@Test
void placeOrder_発行されたイベントをdoAnswerで拾う() {
List<Object> published = new ArrayList<>();
doAnswer(inv -> {
published.add(inv.getArgument(0));
return null; // voidなのでnullを返す
}).when(eventPublisher).publishEvent(any(OrderPlacedEvent.class));
when(paymentClient.charge(1000, "JPY")).thenReturn(new PaymentResult("tx-123"));
when(orderRepository.save(any(Order.class))).thenAnswer(inv -> inv.getArgument(0));
orderService.placeOrder(request);
assertThat(published).hasSize(1);
assertThat(published.get(0)).isInstanceOf(OrderPlacedEvent.class);
}
Complex dependencies such as those that invoke callbacks require doAnswer, but if you “just want to see the value that was passed,” the ArgumentCaptor described later is simpler.
Verifying Calls with verify
Stubs were the behavior setup. verify is what checks “was it called” or “was it not called.” Let’s add verification to the payment failure test.
@Test
void placeOrder_決済失敗なら保存もイベント発行もしない() {
when(paymentClient.charge(anyInt(), anyString()))
.thenThrow(new PaymentException("card declined"));
assertThrows(PaymentException.class, () -> orderService.placeOrder(request));
verify(paymentClient).charge(1000, "JPY"); // times(1) と同じ意味
verify(orderRepository, never()).save(any()); // 保存されていない
verifyNoInteractions(eventPublisher); // 一切触られていない
}
verify(mock).method() is shorthand for times(1). When you want to specify the count explicitly, use times(2); for upper or lower bounds, there are atMost(n) and atLeastOnce(). never() means “not called,” and verifyNoInteractions() verifies that the mock was never touched at all. Unlike stubs, verify is written after executing the code under test.
The similarly named verifyNoMoreInteractions() guarantees that “there are no calls other than those already verified,” but it tends to produce brittle tests that break with even a small implementation change. Limit its use to places where leaked side effects are a genuine problem.
The matcher-mixing rule from the previous section applies as-is to verify arguments too. Note that verify(paymentClient).charge(anyInt(), "JPY") fails with the same exception.
Verifying the Payment → Save → Event Publishing Order with InOrder
For placeOrder, the order “pay, then save, then publish the event after saving” is part of the specification. Ordering across multiple mocks can be verified with InOrder.
@Test
void placeOrder_決済してから保存しイベントを発行する() {
when(paymentClient.charge(1000, "JPY")).thenReturn(new PaymentResult("tx-123"));
when(orderRepository.save(any(Order.class))).thenAnswer(inv -> inv.getArgument(0));
orderService.placeOrder(request);
InOrder inOrder = inOrder(paymentClient, orderRepository, eventPublisher);
inOrder.verify(paymentClient).charge(1000, "JPY");
inOrder.verify(orderRepository).save(any(Order.class));
inOrder.verify(eventPublisher).publishEvent(any(OrderPlacedEvent.class));
}
If the implementation publishes the event before saving, it fails with VerificationInOrderFailure. The message starts with “Verification in order failure,” followed by “Wanted but not invoked” and then “Wanted anywhere AFTER following interaction” along with the preceding call, so you can immediately see where the order broke down.
However, InOrder is not something to add to every test. Limit it to places where the order itself is the specification, such as “do not save before payment.”
Verifying the Contents of the Saved Order with ArgumentCaptor
verify(orderRepository).save(any()) only tells you that “it was saved.” When you want to check the status or amount of the saved Order, that’s where ArgumentCaptor comes in.
@Captor ArgumentCaptor<Order> orderCaptor;
@Test
void placeOrder_保存されるOrderの中身を検証する() {
when(paymentClient.charge(1000, "JPY")).thenReturn(new PaymentResult("tx-123"));
when(orderRepository.save(any(Order.class))).thenAnswer(inv -> inv.getArgument(0));
orderService.placeOrder(request);
verify(orderRepository).save(orderCaptor.capture());
Order saved = orderCaptor.getValue();
assertThat(saved.status()).isEqualTo(OrderStatus.PAID);
assertThat(saved.totalAmount()).isEqualTo(1000);
assertThat(saved.transactionId()).isEqualTo("tx-123");
}
Placing capture() as the argument of verify captures the value passed in that call, which you can retrieve with getValue(). If it was called multiple times, getAllValues() returns them as a list.
If you create it locally without @Captor, use ArgumentCaptor.forClass(Order.class). However, types involving generics such as List<Order> produce an unchecked warning with forClass, so declaring a field with @Captor is easier. With Mockito 5.7 or later, you can also leave it to type inference with ArgumentCaptor.captor().
A @Captor field remains null unless Mockito annotations are initialized with MockitoExtension or MockitoAnnotations.openMocks(this). If you get a NullPointerException at capture(), check that first.
The OrderPlacedEvent we picked up with doAnswer earlier can likewise be captured with ArgumentCaptor<OrderPlacedEvent>. A simple rule of thumb suffices: Captor if you just want to look at the value, doAnswer if you want something to happen the moment it’s called.
How to Read and Fix UnnecessaryStubbingException
MockitoExtension runs in Strict Stubs mode by default. If there is a stub that was never used during the test, the test is failed at the end even if the test itself passed.
For example, if you leave a save() stub copied from the happy path in the payment failure test, you get this:
org.mockito.exceptions.misusing.UnnecessaryStubbingException:
Unnecessary stubbings detected.
Clean & maintainable test code requires zero unnecessary code.
Following stubbings are unnecessary (click to navigate to relevant line of code):
1. -> at OrderServiceTest.placeOrder_決済失敗なら保存もイベント発行もしない(OrderServiceTest.java:58)
Please remove unnecessary stubbings or use 'lenient' strictness. More info: javadoc for UnnecessaryStubbingException class.
With JUnit 4’s MockitoJUnitRunner, the first line includes the class name as “Unnecessary stubbings detected in test class: OrderServiceTest,” but with MockitoExtension the class name is not included. When searching, use the wording from the second line onward to hit both.
Since save() is never reached once the payment throws, that stub is dead. Strict Stubs judges this as “the test’s intent is ambiguous” and fails it. It may feel noisy, but it’s a mechanism that detects stubs made unnecessary by refactoring, so the right attitude is to treat it as a failure rather than a warning.
The fix is simply to delete the stub on the indicated line from that test. When this error occurs frequently, the cause is almost always a design that bundles stubs shared by all tests into @BeforeEach. Put stubs inside the test methods that use them.
If you absolutely must keep a shared stub, you can relax it with lenient().when(...) or @MockitoSettings(strictness = Strictness.LENIENT). But this is an escape hatch, and making the whole class LENIENT eliminates the benefits of Strict Stubs. If you use it, limit it to the few lines of shared setup.
Choosing Between @Mock + @InjectMocks and @MockitoBean
With the approach so far, you can write the vast majority of Service-layer tests. So when do you need a Spring context? When you want to verify behavior that includes Spring’s proxy mechanics, such as @Transactional rollback, @Async asynchronous execution, or the receiving side of @EventListener.
In that case, start the context with @SpringBootTest and replace only the dependencies with mocks using @MockitoBean.
@SpringBootTest
class OrderServiceIntegrationTest {
@MockitoBean PaymentClient paymentClient; // コンテキスト内のBeanをモックに差し替え
@Autowired OrderService orderService;
@Test
void placeOrder_コンテキスト上のBeanでchargeが呼ばれる() {
when(paymentClient.charge(anyInt(), eq("JPY")))
.thenReturn(new PaymentResult("tx-123"));
orderService.placeOrder(new OrderRequest("user-1", 500, 2));
verify(paymentClient).charge(1000, "JPY");
}
}
In this example, OrderRepository is started as a real Bean, so if it’s a JPA repository, a DataSource (DB) is required. Moreover, with the record version of Order from this article, it cannot be treated as a JPA entity and the context will not start. To actually run it, prepare a JPA entity version of Order and a DB. If you just want to check Spring’s mechanics without involving a DB, replacing OrderRepository with @MockitoBean as well is sufficient. If you want to verify with the DB included, the Testcontainers article is a useful reference.
@MockitoBean is the name since Spring Boot 3.4, and the earlier @MockBean has been deprecated. The migration steps are summarized in the migration article from @MockBean to @MockitoBean.
The important point is that even when you switch to @MockitoBean, the way you write when / verify / ArgumentCaptor is exactly the same as in this article. The only things that change are how the mock is created and whether a context is started.
@Mock + @InjectMocks | @SpringBootTest + @MockitoBean | |
|---|---|---|
| Startup time | Tens of milliseconds | Several seconds or more (context startup) |
| What is verified | Service logic only | Includes proxies, transactions, and listeners |
| Best suited for | The majority of Service-layer tests | Checking behavior that involves Spring’s mechanics |
Real communication with external APIs is covered in the WireMock article, and the overall picture of test annotations is left to the cheat sheet.
The Complete OrderServiceTest
Here are the tests written throughout the article, combined into a single class. The comment directly above each method indicates the corresponding section.
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock OrderRepository orderRepository;
@Mock PaymentClient paymentClient;
@Mock ApplicationEventPublisher eventPublisher;
@Captor ArgumentCaptor<Order> orderCaptor;
@InjectMocks OrderService orderService;
OrderRequest request;
@BeforeEach
void setUp() {
request = new OrderRequest("user-1", 500, 2);
}
// when/thenReturnで戻り値をスタブする
@Test
void placeOrder_決済成功ならPAIDの注文を返す() {
when(paymentClient.charge(1000, "JPY")).thenReturn(new PaymentResult("tx-123"));
when(orderRepository.save(any(Order.class))).thenAnswer(inv -> inv.getArgument(0));
Order order = orderService.placeOrder(request);
assertThat(order.status()).isEqualTo(OrderStatus.PAID);
assertThat(order.transactionId()).isEqualTo("tx-123");
}
// verifyで呼び出しを検証する
@Test
void placeOrder_決済失敗なら保存もイベント発行もしない() {
when(paymentClient.charge(anyInt(), anyString()))
.thenThrow(new PaymentException("card declined"));
assertThrows(PaymentException.class, () -> orderService.placeOrder(request));
verify(orderRepository, never()).save(any());
verifyNoInteractions(eventPublisher);
}
// voidメソッドはdoThrowでスタブする
@Test
void cancelOrder_返金失敗ならステータスを更新しない() {
Order paid = new Order(1L, "user-1", 1000, "tx-123", OrderStatus.PAID);
when(orderRepository.findById(1L)).thenReturn(Optional.of(paid));
doThrow(new PaymentException("refund failed")).when(paymentClient).refund("tx-123");
assertThrows(PaymentException.class, () -> orderService.cancelOrder(1L));
verify(orderRepository, never()).save(any());
}
// InOrderで順番を検証する
@Test
void placeOrder_決済してから保存しイベントを発行する() {
when(paymentClient.charge(1000, "JPY")).thenReturn(new PaymentResult("tx-123"));
when(orderRepository.save(any(Order.class))).thenAnswer(inv -> inv.getArgument(0));
orderService.placeOrder(request);
InOrder inOrder = inOrder(paymentClient, orderRepository, eventPublisher);
inOrder.verify(paymentClient).charge(1000, "JPY");
inOrder.verify(orderRepository).save(any(Order.class));
inOrder.verify(eventPublisher).publishEvent(any(OrderPlacedEvent.class));
}
// ArgumentCaptorで保存内容を検証する
@Test
void placeOrder_保存されるOrderの中身を検証する() {
when(paymentClient.charge(1000, "JPY")).thenReturn(new PaymentResult("tx-123"));
when(orderRepository.save(any(Order.class))).thenAnswer(inv -> inv.getArgument(0));
orderService.placeOrder(request);
verify(orderRepository).save(orderCaptor.capture());
Order saved = orderCaptor.getValue();
assertThat(saved.status()).isEqualTo(OrderStatus.PAID);
assertThat(saved.totalAmount()).isEqualTo(1000);
}
}
@BeforeEach only creates the OrderRequest, and all stubs are inside the test methods. Keep this shape and Strict Stubs will almost never complain.
Summary
Mockito has many features, but you won’t get lost if you think of them in three roles. when and the do family are behavior setup, verify is call verification, and ArgumentCaptor is verification of the values that were passed.
InvalidUseOfMatchersException and UnnecessaryStubbingException are both exceptions that detect configuration mistakes on the test code side. Read the message, and you can fix it on the spot with “make every argument a matcher” or “remove the unused stub.”
This article did not cover @Spy, mockStatic, or the BDD-style given / willReturn. Refer to the official Javadoc when you need them. The underlying approach is the same as in this article.
For Controller tests, head to the MockMvc article. If you want to go back to the basics, read the introductory article alongside this one.