@Mock と @InjectMocks で最初のService層テストは書けた。でもその次の一手でつまずいていませんか。voidメソッドをスタブしようとしてコンパイルが通らない、例外を投げさせる書き方が分からない、保存されたエンティティの中身を確かめたいのに手段がない、そしてよく分からない UnnecessaryStubbingException に怒られる。このあたりは、私も最初は毎回検索していました。
この記事では、Repository・外部決済API・イベント発行の3つに依存する OrderService を1つ用意して、そのテストスイートを段階的に完成させていきます。途中で InvalidUseOfMatchersException と UnnecessaryStubbingException にわざと遭遇し、メッセージの読み方と直し方も押さえます。
JUnit 5の基本とwhen/verifyの初歩は 入門記事 で扱っているので、本記事ではその先に絞ります。検証環境は Spring Boot 3.5系 / JUnit 5 / Mockito 5系(spring-boot-starter-test に同梱)です。
題材となるOrderServiceと依存関係
以降のテストはすべて、この OrderService に対して書きます。placeOrder は金額計算、決済、保存、イベント発行の順に進み、cancelOrder は注文を取得して返金し、ステータスを更新して保存します。
@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));
}
}
周辺のクラスは最小限にしています。実際の Order はJPAエンティティになるでしょうが、Mockitoの使い方に集中したいので記事ではrecordで簡略化しています。2つの例外はどちらも RuntimeException を継承させている点に注意してください。charge() や refund() は throws を宣言していないので、後述の thenThrow / doThrow で投げられるのは非チェック例外だけです。チェック例外を渡すと 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); }
}
Springコンテキストなしでテストを組む
Service層の単体テストに @SpringBootTest は要りません。@ExtendWith(MockitoExtension.class) を付けたクラスに @Mock で依存を宣言し、@InjectMocks で OrderService を組み立てれば、起動は数十ミリ秒で終わります。
@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 はまずコンストラクタ経由で注入しようとします。OrderService がコンストラクタインジェクションなので、3つのモックが型で解決されてそのまま渡されます。フィールドインジェクションのServiceだと注入の挙動が読みにくくなるので、テストしやすさの面でもコンストラクタ注入をおすすめします。詳しくは DIの3方式の比較記事 をどうぞ。
なお、@InjectMocks は注入できない依存があっても例外を出さず、そのフィールドが null のまま進みます。テスト中に NullPointerException が出たら、まずモックの宣言漏れを疑ってください。
@BeforeEach には共通の OrderRequest を作るだけにして、スタブは各テストメソッドの中に置きます。この方針は後述のStrict Stubsと相性が良いです。
when/thenReturnで戻り値をスタブする
まずは正常系を完成させましょう。charge() が決済成功を返し、save() は渡された Order をそのまま返すようにスタブします。
@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 は固定値を返し、thenAnswer は呼び出し時の引数を使って戻り値を組み立てます。save() のように「入れたものが返ってくる」振る舞いは thenAnswer が便利です。
thenReturn をチェーンすると、呼び出しごとに順番に違う値を返してくれます。OrderService にはリトライがないので、ここではモックを直接呼んで挙動だけ示します。「1回目は空、2回目は値あり」のような状況を作りたいときに使うものだと覚えておいてください。
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回目以降はずっと最後の値
thenReturn(a, b) と可変長引数で書くこともできますが、Optional のようなジェネリクス型では未検査警告(unchecked generic array creation)が出るので、チェーン形式の方がすっきりします。
例外を返したいときは thenThrow です。決済失敗を再現して、placeOrder がそのまま例外を伝播することを確かめます。
@Test
void placeOrder_決済失敗なら例外を伝播する() {
when(paymentClient.charge(anyInt(), anyString()))
.thenThrow(new PaymentException("card declined"));
assertThrows(PaymentException.class, () -> orderService.placeOrder(request));
}
ここで役割を一言で整理しておくと、when や後述の do 系は「このモックはこう振る舞う」というGivenの準備で、verify は「呼ばれたかどうか」を確かめるThenです。この2つを混同しないだけで、テストがかなり読みやすくなります。
引数マッチャany/eq/argThatとInvalidUseOfMatchersException
先ほどの anyInt() や any(Order.class) が引数マッチャです。よく使うのは「型だけ合えばよい」「一部だけ固定したい」「条件付き」の3パターンで、ハマりやすいのがマッチャと実値の混在です。「金額は何でもいいけど通貨はJPY」と書きたくて、つい 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"));
ルールは「1つでもマッチャを使うなら、全引数をマッチャにする」です。実値は eq("JPY") で包めば済むので、上のNG例も charge(anyInt(), eq("JPY")) に直せば通ります。
null の扱いには注意点があります。any(Order.class) のような型指定つきの any は、Mockito 2.1.0以降 null にマッチしません。anyInt() や anyString() も同じです。一方、引数なしの any() は null を含む任意の値にマッチします。null を渡す呼び出しを意図的にスタブしたいなら、isNull() で明示しておくと読み手にも意図が伝わります。
この例外が出る場面は2つに分けて読んでください。上のように実値とマッチャを混在させた場合は、モック呼び出しの時点でマッチャ数と引数数が照合されるので、その行で即座に失敗します。一方、when や verify の外で any() などを呼んでしまった場合(実オブジェクトのメソッド引数に渡したときなど)は、マッチャがスレッドに記録されたまま残ります。MockitoExtension を使っていれば、テスト終了時に validateMockitoUsage() が実行され、「Misplaced or misused argument matcher detected here」として 同じテストの末尾 で報告されます。拡張もランナーも使わず MockitoAnnotations.openMocks(this) だけで初期化している場合は、次にモックへ触れた別のテストで報告されることがあるので、心当たりのないメソッド名が出たら直前のテストを疑ってください。
voidメソッドはdoThrow/doNothing/doAnswerでスタブする
refund() や publishEvent() はvoidです。when(paymentClient.refund("tx-123")) と書くと、voidの呼び出しは式として when() に渡せないのでコンパイルエラーになります。voidメソッドは do〜().when(mock).method() の順で書きます。
cancelOrder の異常系として、返金に失敗したらステータスを更新して保存しないことを検証しましょう。
@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() はモックのデフォルト動作と同じなので、単独で書く意味はほぼありません。doThrow(...).doNothing().when(...) のように、1回目だけ失敗させて2回目から通常動作に戻す場面で使います。
副作用を再現したいときは doAnswer です。publishEvent() に渡されたイベントをListに溜めておいて、後から中身を確認できます。
@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);
}
コールバックを呼び返すような複雑な依存には doAnswer が必要ですが、「渡された値を見たいだけ」なら後述の ArgumentCaptor の方がシンプルです。
verifyで呼び出しを検証する
スタブは振る舞いの準備でした。「呼ばれたか」「呼ばれていないか」を確かめるのが verify です。決済失敗テストに検証を足してみます。
@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() は times(1) の省略形です。回数を明示したいときは times(2)、上限や下限なら atMost(n) や atLeastOnce() があります。never() は「呼ばれていない」、verifyNoInteractions() はそのモックが一度も触られていないことの検証です。スタブと違って、verify はテスト対象を実行した後に書きます。
似た名前の verifyNoMoreInteractions() は「verify済み以外の呼び出しがない」ことを保証しますが、実装をちょっと変えるだけで落ちる脆いテストになりがちです。副作用の漏れが本当に問題になる箇所に限って使いましょう。
verify の引数にも、前節のマッチャ混在ルールがそのまま適用されます。verify(paymentClient).charge(anyInt(), "JPY") は同じ例外で落ちるので注意してください。
InOrderで決済→保存→イベント発行の順番を検証する
placeOrder は「決済してから保存し、保存してからイベントを発行する」順序が仕様です。複数モックにまたがる順序は 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));
}
もし実装が保存より先にイベントを発行していると、VerificationInOrderFailure で落ちます。メッセージは「Verification in order failure」で始まり、「Wanted but not invoked」に続いて「Wanted anywhere AFTER following interaction」と直前の呼び出しが表示されるので、どこで順序が崩れたかはすぐ分かります。
ただし InOrder はすべてのテストに付けるものではありません。「決済前に保存しない」のように、順序そのものが仕様である箇所だけに絞ってください。
ArgumentCaptorで保存されたOrderの中身を検証する
verify(orderRepository).save(any()) では「保存された」ことしか分かりません。保存された Order のステータスや金額まで確かめたいときが ArgumentCaptor の出番です。
@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");
}
capture() を verify の引数に置くと、その呼び出しで渡された値が捕捉され、getValue() で取り出せます。複数回呼ばれた場合は getAllValues() でリストとして受け取れます。
@Captor を使わずにローカルで作るなら ArgumentCaptor.forClass(Order.class) です。ただし List<Order> のようなジェネリクスを含む型は forClass だと未検査警告が出るので、フィールドに @Captor で宣言する方が楽です。Mockito 5.7以降なら ArgumentCaptor.captor() でも型推論に任せられます。
@Captor フィールドは、MockitoExtension か MockitoAnnotations.openMocks(this) でMockitoアノテーションを初期化していないと null のままです。capture() で NullPointerException が出たら、まずそこを確認しましょう。
先ほど doAnswer で拾った OrderPlacedEvent も、同じように ArgumentCaptor<OrderPlacedEvent> で捕捉できます。値を見るだけなら Captor、呼ばれた瞬間に何かをさせたいなら doAnswer、という使い分けで十分です。
UnnecessaryStubbingExceptionが出たときの読み方と直し方
MockitoExtension はデフォルトで Strict Stubs モードです。テスト中に一度も使われなかったスタブがあると、テスト自体は通っていても最後に落とされます。
たとえば決済失敗テストに、正常系からコピーした save() のスタブを残しておくとこうなります。
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.
JUnit 4の MockitoJUnitRunner では1行目が「Unnecessary stubbings detected in test class: OrderServiceTest」とクラス名付きになりますが、MockitoExtension ではクラス名は付きません。検索するときは2行目以降の文言を使うと両方ヒットします。
決済で例外が出た時点で save() には到達しないので、そのスタブは死んでいます。Strict Stubsはこれを「テストの意図が曖昧」と判断して失敗にします。うるさく感じるかもしれませんが、リファクタで不要になったスタブを検出してくれる仕組みなので、警告ではなく失敗として受け止めるのが正解です。
直し方は、表示された行のスタブをそのテストから削除するだけです。このエラーが多発する場合、@BeforeEach に全テスト共通のスタブをまとめている設計が原因になっていることがほとんどです。スタブは使うテストメソッドの中に置きましょう。
どうしても共通スタブを残したい場合は lenient().when(...) や @MockitoSettings(strictness = Strictness.LENIENT) で緩められます。ただしこれは逃げ道で、クラス全体をLENIENTにするとStrict Stubsの恩恵が消えます。使うなら共通セットアップの数行に限定してください。
@Mock+@InjectMocksと@MockitoBeanの使い分け
ここまでの形式で、Service層のテストの大半は書けます。ではSpringコンテキストが必要になるのはどんなときでしょうか。@Transactional のロールバック、@Async の非同期実行、@EventListener の受け取り側など、Springのプロキシ動作まで含めて検証したい場合です。
その場合は @SpringBootTest でコンテキストを起動し、@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");
}
}
この例では OrderRepository を実Beanのまま起動しているので、JPAリポジトリならDataSource(DB)が必要です。しかも本記事のrecord版 Order のままではJPAエンティティとして扱えず、コンテキストが起動しません。実際に動かすにはJPAエンティティ版の Order とDBを用意してください。DBを伴わずにSpringの仕組みだけを確認したいなら、OrderRepository も @MockitoBean で差し替えれば済みます。DBごと検証したい場合は Testcontainers記事 が参考になります。
@MockitoBean は Spring Boot 3.4以降の名称で、それ以前の @MockBean は非推奨になっています。移行手順は @MockBeanから@MockitoBeanへの移行記事 にまとめてあります。
大事なのは、@MockitoBean に変えても when / verify / ArgumentCaptor の書き方は本記事とまったく同じという点です。変わるのはモックの作り方と、コンテキスト起動の有無だけです。
@Mock + @InjectMocks | @SpringBootTest + @MockitoBean | |
|---|---|---|
| 起動時間 | 数十ミリ秒 | 数秒〜(コンテキスト起動) |
| 検証対象 | Serviceのロジックのみ | プロキシ・トランザクション・リスナーを含む |
| 向いている場面 | Service層のテストの大半 | Springの仕組みが絡む振る舞いの確認 |
外部APIとの実通信は WireMock記事、テストアノテーションの全体像は 早見表 に譲ります。
完成したOrderServiceTest全体
本文で書いてきたテストを1つのクラスにまとめます。各メソッドの直前コメントが、対応するセクションです。
@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 は OrderRequest を作るだけで、スタブは全部テストメソッドの中にあります。この形にしておけばStrict Stubsに怒られることはまずありません。
まとめ
Mockitoの機能はたくさんありますが、役割は3つに分けて考えると迷いません。when と do 系は振る舞いの準備、verify は呼び出しの検証、ArgumentCaptor は渡された値の検証です。
InvalidUseOfMatchersException と UnnecessaryStubbingException は、どちらもテストコード側の設定ミスを検出してくれている例外です。メッセージを読めば「全引数をマッチャにする」「使わないスタブを消す」で、その場で直せます。
この記事では @Spy や mockStatic、BDDスタイルの given / willReturn には触れていません。必要になったら公式Javadocを参照してみてください。基本の考え方は本記事と同じです。
Controllerのテストは MockMvc記事 へどうぞ。基本に戻りたくなったら 入門記事 もあわせて読んでみてください。