9
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ひとりJUnitAdvent Calendar 2022

Day 11

僕たちはいつもMockitoに怒られている

Last updated at Posted at 2022-12-21

はじめに

ひとりJUnitアドベントカレンダー11日目の記事です。
もう22日なのにあと14日分あります。夏休みの宿題は9月の提出日前日まで粘るタイプ。

使用バージョン

pom.xml
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
$ mvn dependency:tree | grep mockito
[INFO] |  +- org.mockito:mockito-junit-jupiter:jar:3.1.0:test
[INFO] |  +- org.mockito:mockito-core:jar:3.1.0:test

Mockitoの怒り

無駄なMock作ってんじゃねえ!とか、全然呼ばれてねえぞ!とか、
事あるごとにぷんぷん怒ってくれるMockitoさん、いつもお世話になっております。
いずれも丁寧なメッセージで、「お前こう書いてるけどこう書くのが正解だよ」と
正しい記述例までエラメに載ってたりして、ちゃんと読めばほぼハマりません。

メッセージをよくよく読んでみると、どうせぬるぽで落ちるような場合でも
事前にMockitoの独自例外を投げて「ここで落とした方がデバッグしやすいでしょ?
変にハマらないように先に例外投げといたから確認してね」とか出力してたりして、
本当にお優しいライブラリでございます。今回はそのラインナップを一部紹介します。

実際のログ+どういうテストだと起きるかという実例のセットで並べていきます。

前提

テスト対象クラスはこちら。

HogeService
@Service
@RequiredArgsConstructor
public class HogeService {

  @NonNull
  private MogeService mogeService;

  public void execute() {
    System.out.println(mogeService.returnArg("hoge", "moge"));
    System.out.println(mogeService.returnArg("foo", "bar"));
  }
}

InvalidUseOfMatchersException

ログ
org.mockito.exceptions.misusing.InvalidUseOfMatchersException: 
Invalid use of argument matchers!
2 matchers expected, 1 recorded:

This exception may occur if matchers are combined with raw values:
    //incorrect:
    someMethod(anyObject(), "raw String");
When using matchers, all arguments have to be provided by matchers.
For example:
    //correct:
    someMethod(anyObject(), eq("String by matcher"));
  @Test
  void InvalidUseOfMatchersException1() {
    hogeService.execute();
    verify(mogeService, times(1)).returnArg("hoge", eq("moge"));
  }

引数の片方は実際の値、もう片方はMatcher(any()とかeq()とか)を使っている場合、
両方とも実際の値にするかMatcherにするか統一しろと怒ってくれます。
別記事でも同じようなことを書いているので合わせてご参照ください。

ログ
org.mockito.exceptions.misusing.InvalidUseOfMatchersException: 
Misplaced or misused argument matcher detected here:

You cannot use argument matchers outside of verification or stubbing.
Examples of correct usage of argument matchers:
    when(mock.get(anyInt())).thenReturn(null);
    doThrow(new RuntimeException()).when(mock).someVoidMethod(anyObject());
    verify(mock).someMethod(contains("foo"))

This message may appear after an NullPointerException if the last matcher is returning an object 
like any() but the stubbed method signature expect a primitive argument, in this case,
use primitive alternatives.
    when(mock.get(any())); // bad use, will raise NPE
    when(mock.get(anyInt())); // correct usage use

Also, this error might show up because you use argument matchers with methods that cannot be mocked.
Following methods *cannot* be stubbed/verified: final/private/equals()/hashCode().
Mocking methods declared on non-public parent classes is not supported.

  @Test
  void InvalidUseOfMatchersException2() {
    doReturn(true).when(mogeService).equals(any());
  }

equals()hashCode()をmock化した場合も同じ例外で怒られます。
メッセージは異なりますが。

  @Test
  void InvalidUseOfMatchersException3() {
    doNothing().when(mogeService).setNumber(any());
  }

プリミティブ型を受け取るメソッドに対してany()を渡した場合も同様です。
any()が返却するのは参照型なので、プリミティブ型を受け取るメソッドに対しては
anyInt()anyBoolean()を使ってください。

ArgumentsAreDifferent

ログ
Argument(s) are different! Wanted:
mogeService.returnArg("hog", "moge");
Actual invocations have different arguments:
mogeService.returnArg("hoge", "moge");
mogeService.returnArg("foo", "bar");
  @Test
  void ArgumentsAreDifferent() {
    hogeService.execute();
    verify(mogeService, times(1)).returnArg(eq("hog"), eq("moge"));
  }

verify()において、呼び出し時の想定引数と実際の引数が異なる場合に怒られます。
今回は("hoge", "moge")で呼んでいるため、第一引数の期待値"hog"と一致しません。

PotentialStubbingProblem

ログ
org.mockito.exceptions.misusing.PotentialStubbingProblem: 
Strict stubbing argument mismatch. Please check:
 - this invocation of 'returnArg' method:
    mogeService.returnArg("hoge", "moge");
 - has following stubbing(s) with different arguments:
    1. mogeService.returnArg("", null);
Typically, stubbing argument mismatch indicates user mistake when writing tests.
Mockito fails early so that you can debug potential problem easily.
However, there are legit scenarios when this exception generates false negative signal:
  - stubbing the same method multiple times using 'given().will()' or 'when().then()' API
    Please use 'will().given()' or 'doReturn().when()' API for stubbing.
  - stubbed method is intentionally invoked with different arguments by code under test
    Please use default or 'silent' JUnit Rule (equivalent of Strictness.LENIENT).
For more information see javadoc for PotentialStubbingProblem class.
  @Test
  void PotentialStubbingProblem() {
    when(mogeService.returnArg(anyString(), eq("xxx"))).thenReturn("bar");
    hogeService.execute();
  }

特定の引数が渡された場合のMockの挙動を設定したものの、
その引数では呼ばれずに別の引数で呼び出されている場合に怒られます。
メッセージは潜在的な不具合を埋め込まないように早めに検知しといたよという趣旨。
この検知を行わないように設定することもできるようです。

Please use default or 'silent' JUnit Rule (equivalent of Strictness.LENIENT).

Mockの挙動設定で条件とした引数が実際と異なる場合はPotentialStubbingProblem
呼出回数検証で条件とした引数が実際と異なる場合は上のArgumentsAreDifferentです。

TooFewActualInvocations

ログ
org.mockito.exceptions.verification.TooFewActualInvocations: 
mogeService.returnArg("hoge", "moge");
Wanted 2 times:
But was 1 time:
  @Test
  void TooFewActualInvocations() {
    hogeService.execute();
    verify(mogeService, times(2)).returnArg(eq("hoge"), eq("moge"));
  }

想定する呼出回数よりも実際の呼出回数が少ないときに怒られます。

TooManyActualInvocations

ログ
org.mockito.exceptions.verification.TooManyActualInvocations: 
mogeService.returnArg(<any>, <any>);
Wanted 1 time:
But was 2 times:
  @Test
  void TooManyActualInvocations() {
    hogeService.execute();
    verify(mogeService, times(1)).returnArg(any(), any());
  }

想定する呼出回数よりも実際の呼出回数が多くても怒られます。

NeverWantedButInvoked

ログ
org.mockito.exceptions.verification.NeverWantedButInvoked: 
mogeService.returnArg("hoge", "moge");
Never wanted here:
But invoked here:
  @Test
  void NeverWantedButInvoked() {
    hogeService.execute();
    verify(mogeService, times(0)).returnArg(eq("hoge"), eq("moge"));
  }

一回も呼ばれないでほしいのに呼ばれている場合に怒られます。
一つ前の例外と同じエラーでも通じるのに、想定回数が0回かどうかで変えていて芸が細かい。

Wanted but not invoked

ログ
Wanted but not invoked:
mogeService.returnStr();
  @Test
  void wantedButNotInvoked() {
    hogeService.execute();
    verify(mogeService, times(1)).returnStr();
  }

こちらは逆に一回は呼ばれてほしいものが一回も呼ばれなかったケース。

CannotStubVoidMethodWithReturnValue

ログ
org.mockito.exceptions.misusing.CannotStubVoidMethodWithReturnValue: 
'returnNothing' is a *void method* and it *cannot* be stubbed with a *return value*!
Voids are usually stubbed with Throwables:
    doThrow(exception).when(mock).someVoidMethod();
If you need to set the void method to do nothing you can use:
    doNothing().when(mock).someVoidMethod();
For more information, check out the javadocs for Mockito.doNothing().
***
If you're unsure why you're getting above error read on.
Due to the nature of the syntax above problem might occur because:
1. The method you are trying to stub is *overloaded*. Make sure you are calling the right overloaded version.
2. Somewhere in your test you are stubbing *final methods*. Sorry, Mockito does not verify/stub final methods.
3. A spy is stubbed using when(spy.foo()).then() syntax. It is safer to stub spies - 
   - with doReturn|Throw() family of methods. More in javadocs for Mockito.spy() method.
4. Mocking methods declared on non-public parent classes is not supported.
  @Test
  void CannotStubVoidMethodWithReturnValue() {
    doReturn("hoge").when(mogeService).returnNothing();
  }

voidのメソッドのMockにdoReturn()で戻り値を設定すると怒られます。

Only void methods can doNothing()!

ログ
org.mockito.exceptions.base.MockitoException: 
Only void methods can doNothing()!
Example of correct use of doNothing():
    doNothing().
    doThrow(new RuntimeException())
    .when(mock).someVoidMethod();
Above means:
someVoidMethod() does nothing the 1st time but throws an exception the 2nd time is called
  @Test
  void onlyVoidMethodsCanBeSetToDoNothing() {
    doNothing().when(mogeService).returnStr();
  }

一つ前の逆。
戻り値があるメソッドのMockにdoNothing()を設定すると怒られます。

WrongTypeOfReturnValue

ログ
org.mockito.exceptions.misusing.WrongTypeOfReturnValue: 
Integer cannot be returned by returnStr()
returnStr() should return String
***
If you're unsure why you're getting above error read on.
Due to the nature of the syntax above problem might occur because:
1. This exception *might* occur in wrongly written multi-threaded tests.
   Please refer to Mockito FAQ on limitations of concurrency testing.
2. A spy is stubbed using when(spy.foo()).then() syntax. It is safer to stub spies - 
   - with doReturn|Throw() family of methods. More in javadocs for Mockito.spy() method.
  @Test
  void WrongTypeOfReturnValue() {
    doReturn(999).when(mogeService).returnStr();
  }

実際のメソッドの戻り値の型と、Mockの挙動として設定した戻り値の型が相違している場合。
上の例ではStringを返すメソッドにintを返させようとしています。

UnfinishedStubbingException

ログ
org.mockito.exceptions.misusing.UnfinishedStubbingException: 
Unfinished stubbing detected here:

E.g. thenReturn() may be missing.
Examples of correct stubbing:
    when(mock.isOk()).thenReturn(true);
    when(mock.isOk()).thenThrow(exception);
    doThrow(exception).when(mock).someVoidMethod();
Hints:
 1. missing thenReturn()
 2. you are trying to stub a final method, which is not supported
 3. you are stubbing the behaviour of another mock inside before 'thenReturn' instruction is completed
  @Test
  void UnfinishedStubbingException() {
    doReturn(true).when(mogeService);
  }

doReturn(返却値).when(mock化したクラス).メソッド()と書くべきところ、
メソッドを生やしていないために怒られるケース。

No argument value was captured!

ログ
org.mockito.exceptions.base.MockitoException: 
No argument value was captured!
You might have forgotten to use argument.capture() in verify()...
...or you used capture() in stubbing but stubbed method was not called.
Be aware that it is recommended to use capture() only with verify()

Examples of correct argument capturing:
    ArgumentCaptor<Person> argument = ArgumentCaptor.forClass(Person.class);
    verify(mock).doSomething(argument.capture());
    assertEquals("John", argument.getValue().getName());
  @Test
  void noArgumentValueWasCaptured() {
    ArgumentCaptor<Student> captor = ArgumentCaptor.forClass(Student.class);
    captor.getValue();
  }

ArgumentCaptorを用意してgetValue()を呼んだんだけれども、
そもそもcapture()を呼んでないから何も捕捉してませんよと怒られています。
verifyの中でcapture()しましょう。

UnnecessaryStubbingException

ログ
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):
Please remove unnecessary stubbings or use 'lenient' strictness. More info: javadoc for UnnecessaryStubbingException class.
  @Test
  void UnnecessaryStubbingException() {
    when(mogeService.returnArg(anyString(), anyString())).thenReturn("bar");
  }

mock化して挙動まで設定したはいいもののそのメソッドが呼ばれなかった場合に怒られます。
無駄な処理消した方がメンテしやすい綺麗なコードになるよと教えてくれています。優しい。

まとめ

修正例も載せればよかったかなと思いましたが、いずれもシンプルな事象なのでまあ妥協。
Mockito入門したての方々がエラーメッセージで検索した時の一助となれば幸いです。

9
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?