日常のMockitoへのユニットテスト実践者ガイド
公開: 2022-03-11アジャイルの時代には単体テストが必須になり、自動テストに役立つ多くのツールが利用可能になっています。 そのようなツールの1つがMockitoです。これは、テスト用のモックオブジェクトを作成および構成できるオープンソースフレームワークです。
この記事では、モックの作成と構成、およびそれらを使用してテスト対象のシステムの予想される動作を検証する方法について説明します。 また、Mockitoの内部を少し掘り下げて、その設計と警告をよりよく理解します。 ユニットテストフレームワークとしてJUnitを使用しますが、MockitoはJUnitにバインドされていないため、別のフレームワークを使用している場合でも従うことができます。
Mockitoの入手
最近、Mockitoを入手するのは簡単です。 Gradleを使用している場合は、ビルドスクリプトに次の1行を追加するだけです。
testCompile "org.mockito:mockito−core:2.7.7"
まだMavenを好む私のような人は、次のように依存関係にMockitoを追加するだけです。
<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>2.7.7</version> <scope>test</scope> </dependency>
もちろん、世界はMavenやGradleよりもはるかに広いです。 プロジェクト管理ツールを自由に使用して、Maven中央リポジトリからMockitojarアーティファクトをフェッチできます。
モッキートに近づく
単体テストは、依存関係の動作に依存することなく、特定のクラスまたはメソッドの動作をテストするように設計されています。 コードの最小の「ユニット」をテストしているので、これらの依存関係の実際の実装を使用する必要はありません。 さらに、さまざまな動作をテストするときに、これらの依存関係のわずかに異なる実装を使用します。 これに対する従来のよく知られたアプローチは、「スタブ」(特定のシナリオに適したインターフェイスの特定の実装)を作成することです。 このような実装には通常、ハードコードされたロジックがあります。 スタブは一種のテストダブルです。 他の種類には、偽物、モック、スパイ、ダミーなどが含まれます。
Mockitoで頻繁に使用されているため、「モック」と「スパイ」の2種類のテストダブルのみに焦点を当てます。
モック
モックとは何ですか? 明らかに、それはあなたが仲間の開発者をからかう場所ではありません。 単体テストのモックは、制御された方法で実際のサブシステムの動作を実装するオブジェクトを作成する場合です。 つまり、依存関係の代わりにモックが使用されます。
Mockitoを使用すると、モックを作成し、特定のメソッドが呼び出されたときに何をするかをMockitoに指示してから、テストで本物の代わりにモックインスタンスを使用します。 テスト後、モックにクエリを実行して、呼び出された特定のメソッドを確認したり、状態の変更という形で副作用を確認したりできます。
デフォルトでは、Mockitoはモックのすべてのメソッドの実装を提供します。
スパイ
スパイは、Mockitoが作成するもう1つのタイプのテストダブルです。 モックとは対照的に、スパイを作成するには、スパイするインスタンスが必要です。 デフォルトでは、スパイはすべてのメソッド呼び出しを実際のオブジェクトに委任し、呼び出されたメソッドとパラメーターを記録します。 それがそれをスパイにしているのです:それは実際のオブジェクトをスパイしています。
可能な限り、スパイの代わりにモックを使用することを検討してください。 スパイは、簡単にテストできるように再設計できないレガシーコードのテストに役立つ場合がありますが、スパイを使用してクラスを部分的にモックする必要があることは、クラスがやりすぎていることを示しているため、単一責任の原則に違反しています。
簡単な例の作成
テストを書くことができる簡単なデモを見てみましょう。 識別子でユーザーを検索する単一のメソッドを持つUserRepository
インターフェースがあるとします。 また、クリアテキストのパスワードをパスワードハッシュに変換するパスワードエンコーダーの概念もあります。 UserRepository
とPasswordEncoder
はどちらも、コンストラクターを介して注入されたUserService
の依存関係(コラボレーターとも呼ばれます)です。 デモコードは次のようになります。
UserRepository
public interface UserRepository { User findById(String id); }
ユーザー
public class User { private String id; private String passwordHash; private boolean enabled; public User(String id, String passwordHash, boolean enabled) { this.id = id; this.passwordHash = passwordHash; this.enabled = enabled; } ... }
PasswordEncoder
public interface PasswordEncoder { String encode(String password); }
UserService
public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; } public boolean isValidUser(String id, String password) { User user = userRepository.findById(id); return isEnabledUser(user) && isValidPassword(user, password); } private boolean isEnabledUser(User user) { return user != null && user.isEnabled(); } private boolean isValidPassword(User user, String password) { String encodedPassword = passwordEncoder.encode(password); return encodedPassword.equals(user.getPasswordHash()); } }
このサンプルコードはGitHubにあるので、この記事と一緒にレビュー用にダウンロードできます。
Mockitoを適用する
サンプルコードを使用して、Mockitoを適用し、いくつかのテストを作成する方法を見てみましょう。
モックの作成
Mockitoを使用すると、静的メソッドMockito.mock()
を呼び出すのと同じくらい簡単にモックを作成できます。
import static org.mockito.Mockito.*; ... PasswordEncoder passwordEncoder = mock(PasswordEncoder.class);
Mockitoの静的インポートに注意してください。 この記事の残りの部分では、このインポートが追加されたと暗黙的に見なします。
インポート後、インターフェースであるPasswordEncoder
をモックアウトします。 Mockitoは、インターフェースだけでなく、抽象クラスと具体的な非最終クラスもモックします。 箱から出して、Mockitoはfinalクラスとfinalまたはstaticメソッドをモックすることはできませんが、本当に必要な場合は、Mockito2が実験的なMockMakerプラグインを提供します。
また、メソッドequals()
およびhashCode()
をモックすることはできないことに注意してください。
スパイの作成
スパイを作成するには、Mockitoの静的メソッドspy()
を呼び出して、スパイするインスタンスを渡す必要があります。 返されたオブジェクトのメソッドを呼び出すと、それらのメソッドがスタブされていない限り、実際のメソッドが呼び出されます。 これらの呼び出しは記録され、これらの呼び出しの事実を検証できます( verify()
の詳細な説明を参照してください)。 スパイを作ろう:
DecimalFormat decimalFormat = spy(new DecimalFormat()); assertEquals("42", decimalFormat.format(42L));
スパイを作成することは、モックを作成することと大差ありません。 さらに、モックの構成に使用されるすべてのMockitoメソッドは、スパイの構成にも適用できます。
スパイはモックと比較してめったに使用されませんが、部分的なモックが必要な場合、リファクタリングできないレガシーコードのテストに役立つ場合があります。 そのような場合は、スパイを作成し、そのメソッドの一部をスタブ化して、必要な動作を得ることができます。
デフォルトの戻り値
mock(PasswordEncoder.class)
を呼び出すと、 PasswordEncoder
のインスタンスが返されます。 そのメソッドを呼び出すこともできますが、それらは何を返しますか? デフォルトでは、モックのすべてのメソッドは「初期化されていない」または「空の」値を返します。たとえば、数値型(プリミティブとボックスの両方)の場合はゼロ、ブール型の場合はfalse、その他のほとんどの型の場合はnullを返します。
次のインターフェイスについて考えてみます。
interface Demo { int getInt(); Integer getInteger(); double getDouble(); boolean getBoolean(); String getObject(); Collection<String> getCollection(); String[] getArray(); Stream<?> getStream(); Optional<?> getOptional(); }
次に、モックのメソッドから期待されるデフォルト値を示す次のスニペットについて考えてみます。
Demo demo = mock(Demo.class); assertEquals(0, demo.getInt()); assertEquals(0, demo.getInteger().intValue()); assertEquals(0d, demo.getDouble(), 0d); assertFalse(demo.getBoolean()); assertNull(demo.getObject()); assertEquals(Collections.emptyList(), demo.getCollection()); assertNull(demo.getArray()); assertEquals(0L, demo.getStream().count()); assertFalse(demo.getOptional().isPresent());
スタブ方法
新鮮で変更されていないモックは、まれなケースでのみ役立ちます。 通常、モックを構成し、モックの特定のメソッドが呼び出されたときに何をするかを定義します。 これはスタブと呼ばれます。
Mockitoには2つのスタブ方法があります。 最初の方法は、 「このメソッドが呼び出されたら、何かを実行する」ことです。 次のスニペットについて考えてみます。
when(passwordEncoder.encode("1")).thenReturn("a");
ほぼ英語のように読めます。「 passwordEncoder.encode(“1”)
が呼び出されたら、 a
を返します。」
スタブの2番目の方法は、「このモックのメソッドが次の引数で呼び出されたときに何かを実行する」のようになります。 このスタブの方法は、原因が最後に指定されているため、読みにくくなります。 検討:
doReturn("a").when(passwordEncoder).encode("1");
このスタブのメソッドを使用したスニペットは、次のようになります。「 passwordEncoder
のencode()
メソッドが引数1
で呼び出されa
を返します。」
最初の方法は、タイプセーフであり、読みやすいため、推奨されると考えられています。 ただし、まれに、スパイの実際のメソッドをスタブする場合など、2番目の方法を使用せざるを得ない場合があります。これは、スパイを呼び出すと望ましくない副作用が発生する可能性があるためです。
Mockitoが提供するスタブ方法について簡単に説明します。 例には、両方のスタブ方法を含めます。
戻り値
thenReturn
またはdoReturn()
は、メソッドの呼び出し時に返される値を指定するために使用されます。
//”when this method is called, then do something” when(passwordEncoder.encode("1")).thenReturn("a");
また
//”do something when this mock's method is called with the following arguments” doReturn("a").when(passwordEncoder).encode("1");
連続したメソッド呼び出しの結果として返される複数の値を指定することもできます。 最後の値は、以降のすべてのメソッド呼び出しの結果として使用されます。
//when when(passwordEncoder.encode("1")).thenReturn("a", "b");
また
//do doReturn("a", "b").when(passwordEncoder).encode("1");
次のスニペットでも同じことができます。
when(passwordEncoder.encode("1")) .thenReturn("a") .thenReturn("b");
このパターンは、他のスタブメソッドと一緒に使用して、連続した呼び出しの結果を定義することもできます。
カスタム応答を返す
then()
、 thenAnswer()
のエイリアス、およびdoAnswer()
は同じことを実現します。これは、次のように、メソッドが呼び出されたときに返されるカスタム応答を設定します。
when(passwordEncoder.encode("1")).thenAnswer( invocation -> invocation.getArgument(0) + "!");
また
doAnswer(invocation -> invocation.getArgument(0) + "!") .when(passwordEncoder).encode("1");
thenAnswer()
が取る唯一の引数は、 Answer
インターフェースの実装です。 InvocationOnMock
型のパラメーターを持つ単一のメソッドがあります。
メソッド呼び出しの結果として例外をスローすることもできます。
when(passwordEncoder.encode("1")).thenAnswer(invocation -> { throw new IllegalArgumentException(); });
…またはクラスの実際のメソッドを呼び出します(インターフェースには適用されません):
Date mock = mock(Date.class); doAnswer(InvocationOnMock::callRealMethod).when(mock).setTime(42); doAnswer(InvocationOnMock::callRealMethod).when(mock).getTime(); mock.setTime(42); assertEquals(42, mock.getTime());
あなたがそれが面倒に見えると思うなら、あなたは正しいです。 Mockitoは、テストのこの側面を合理化するために、thenCallRealMethod thenCallRealMethod()
とthenThrow()
を提供します。
実際のメソッドの呼び出し
その名前が示すように、 thenCallRealMethod()
とdoCallRealMethod()
は、モックオブジェクトの実際のメソッドを呼び出します。
Date mock = mock(Date.class); when(mock.getTime()).thenCallRealMethod(); doCallRealMethod().when(mock).setTime(42); mock.setTime(42); assertEquals(42, mock.getTime());
実際のメソッドを呼び出すことは部分的なモックで役立つ場合がありますが、呼び出されたメソッドに不要な副作用がなく、オブジェクトの状態に依存しないことを確認してください。 もしそうなら、スパイはモックよりも適しているかもしれません。
インターフェイスのモックを作成し、実際のメソッドを呼び出すようにスタブを構成しようとすると、Mockitoは非常に有益なメッセージを含む例外をスローします。 次のスニペットについて考えてみます。
when(passwordEncoder.encode("1")).thenCallRealMethod();
Mockitoは次のメッセージで失敗します:
Cannot call abstract real method on java object! Calling real methods is only possible when mocking non abstract method. //correct example: when(mockOfConcreteClass.nonAbstractMethod()).thenCallRealMethod();
このような徹底的な説明を提供するのに十分な配慮をしてくれたMockito開発者に称賛を!
例外のスロー
thenThrow()
とdoThrow()
は、例外をスローするようにモックされたメソッドを構成します。
when(passwordEncoder.encode("1")).thenThrow(new IllegalArgumentException());
また
doThrow(new IllegalArgumentException()).when(passwordEncoder).encode("1");
Mockitoは、スローされた例外がその特定のスタブメソッドに対して有効であることを確認し、例外がメソッドのチェックされた例外リストにない場合は文句を言います。 次のことを考慮してください。
when(passwordEncoder.encode("1")).thenThrow(new IOException());
エラーが発生します:
org.mockito.exceptions.base.MockitoException: Checked exception is invalid for this method! Invalid: java.io.IOException
ご覧のとおり、Mockitoはencode()
がIOException
をスローできないことを検出しました。
例外のインスタンスを渡す代わりに、例外のクラスを渡すこともできます。
when(passwordEncoder.encode("1")).thenThrow(IllegalArgumentException.class);
また
doThrow(IllegalArgumentException.class).when(passwordEncoder).encode("1");
とはいえ、Mockitoは例外インスタンスを検証するのと同じ方法で例外クラスを検証することはできないため、規律を守り、不正なクラスオブジェクトを渡さないようにする必要があります。 たとえば、 encode()
はチェックされた例外をスローすることは期待されていませんが、以下はIOException
をスローします。
when(passwordEncoder.encode("1")).thenThrow(IOException.class); passwordEncoder.encode("1");
デフォルトのメソッドを使用したインターフェイスのモック
インターフェイスのモックを作成するとき、Mockitoはそのインターフェイスのすべてのメソッドをモックすることに注意してください。 Java 8以降、インターフェースには抽象メソッドとともにデフォルトのメソッドが含まれる場合があります。 これらのメソッドもモックされているため、デフォルトのメソッドとして機能するように注意する必要があります。
次の例を考えてみましょう。
interface AnInterface { default boolean isTrue() { return true; } } AnInterface mock = mock(AnInterface.class); assertFalse(mock.isTrue());
この例では、 assertFalse()
が成功します。 それが期待したものではない場合は、次のように、Mockitoに実際のメソッドを呼び出させたことを確認してください。
AnInterface mock = mock(AnInterface.class); when(mock.isTrue()).thenCallRealMethod(); assertTrue(mock.isTrue());
引数マッチャー
前のセクションでは、引数として正確な値を使用してモックメソッドを構成しました。 このような場合、Mockitoは内部でequals()
を呼び出して、期待値が実際の値と等しいかどうかを確認します。
ただし、これらの値が事前にわからない場合もあります。
引数として渡される実際の値を気にしない場合もあれば、より広い範囲の値に対する反応を定義したい場合もあります。 これらすべてのシナリオ(およびそれ以上)は、引数マッチャーを使用して対処できます。 考え方は単純です。正確な値を提供する代わりに、メソッドの引数を照合するためにMockitoの引数マッチャーを提供します。
次のスニペットについて考えてみます。
when(passwordEncoder.encode(anyString())).thenReturn("exact"); assertEquals("exact", passwordEncoder.encode("1")); assertEquals("exact", passwordEncoder.encode("abc"));
最初の行でanyString()
引数マッチャーを使用したため、 encode()
に渡す値に関係なく、結果は同じであることがわかります。 その行を平易な英語で書き直すと、「パスワードエンコーダーが任意の文字列をエンコードするように求められたら、文字列「exact」を返す」ように聞こえます。
Mockitoでは、マッチャーまたは正確な値のいずれかによってすべての引数を指定する必要があります。 したがって、メソッドに複数の引数があり、その引数の一部にのみ引数マッチャーを使用する場合は、それを忘れてください。 次のようなコードを書くことはできません。
abstract class AClass { public abstract boolean call(String s, int i); } AClass mock = mock(AClass.class); //This doesn't work. when(mock.call("a", anyInt())).thenReturn(true);
エラーを修正するには、次のように、最後の行を置き換えa
、のeq
引数マッチャーを含める必要があります。
when(mock.call(eq("a"), anyInt())).thenReturn(true);
ここでは、 eq()
およびanyInt()
引数マッチャーを使用しましたが、他にも多くの利用可能なものがあります。 引数マッチャーの完全なリストについては、 org.mockito.ArgumentMatchers
クラスのドキュメントを参照してください。
検証またはスタブ以外では引数マッチャーを使用できないことに注意することが重要です。 たとえば、次のものを使用することはできません。
//this won't work String orMatcher = or(eq("a"), endsWith("b")); verify(mock).encode(orMatcher);
Mockitoは、誤って配置された引数マッチャーを検出し、 InvalidUseOfMatchersException
をスローします。 引数マッチャーを使用した検証は、次のように実行する必要があります。
verify(mock).encode(or(eq("a"), endsWith("b")));
引数マッチャーも戻り値として使用できません。 MockitoはanyString()
またはany-whateverを返すことはできません。 呼び出しをスタブするときは、正確な値が必要です。
カスタムマッチャー
Mockitoでまだ利用できないマッチングロジックを提供する必要がある場合は、カスタムマッチャーが役に立ちます。 カスタムマッチャーを作成するという決定は、重要な方法で引数を照合する必要があることは、設計上の問題またはテストが複雑になりすぎていることを示しているため、軽視すべきではありません。
そのため、カスタムマッチャーを作成する前に、 isNull()
やnullable()
)などの寛大な引数マッチャーを使用してテストを簡略化できるかどうかを確認する価値があります。 それでも引数マッチャーを作成する必要があると感じる場合は、Mockitoがそれを行うための一連のメソッドを提供します。
次の例を考えてみましょう。
FileFilter fileFilter = mock(FileFilter.class); ArgumentMatcher<File> hasLuck = file -> file.getName().endsWith("luck"); when(fileFilter.accept(argThat(hasLuck))).thenReturn(true); assertFalse(fileFilter.accept(new File("/deserve"))); assertTrue(fileFilter.accept(new File("/deserve/luck")));
ここでは、 hasLuck
引数マッチャーを作成し、 argThat()
を使用して、モックメソッドへの引数としてマッチャーを渡し、ファイル名が「luck」で終わる場合はtrue
を返すようにスタブします。 ArgumentMatcher
を機能インターフェイスとして扱い、ラムダを使用してそのインスタンスを作成できます(これは例で行ったことです)。 簡潔でない構文は次のようになります。
ArgumentMatcher<File> hasLuck = new ArgumentMatcher<File>() { @Override public boolean matches(File file) { return file.getName().endsWith("luck"); } };
プリミティブ型で機能する引数マッチャーを作成する必要がある場合は、 org.mockito.ArgumentMatchers
に他のいくつかのメソッドがあります。
- charThat(ArgumentMatcher <Character>マッチャー)
- booleanThat(ArgumentMatcher <Boolean> matcher)
- byteThat(ArgumentMatcher <Byte> matcher)
- shortThat(ArgumentMatcher <Short> matcher)
- intThat(ArgumentMatcher <Integer> matcher)
- longThat(ArgumentMatcher <Long>マッチャー)
- floatThat(ArgumentMatcher <Float>マッチャー)
- doubleThat(ArgumentMatcher <Double>マッチャー)
マッチャーの組み合わせ
条件が複雑すぎて基本的なマッチャーで処理できない場合は、カスタム引数マッチャーを作成する価値があるとは限りません。 時々マッチャーを組み合わせるとうまくいくでしょう。 Mockitoは、プリミティブ型と非プリミティブ型の両方に一致する引数マッチャーに一般的な論理演算('not'、'および'、'または')を実装するための引数マッチャーを提供します。 これらのマッチャーは、 org.mockito.AdditionalMatchers
クラスの静的メソッドとして実装されます。
次の例を考えてみましょう。
when(passwordEncoder.encode(or(eq("1"), contains("a")))).thenReturn("ok"); assertEquals("ok", passwordEncoder.encode("1")); assertEquals("ok", passwordEncoder.encode("123abc")); assertNull(passwordEncoder.encode("123"));
ここでは、2つの引数マッチャーの結果を組み合わせました: eq("1")
とcontains("a")
。 最後の式or(eq("1"), contains("a"))
は、「引数文字列は「1」に等しいか、「a」を含む必要があります。
geq()
、 leq()
、 gt()
、 lt()
など、 org.mockito.AdditionalMatchers
クラスにリストされているあまり一般的ではないマッチャーがあることに注意してください。これらは、 java.lang.Comparable
。
動作の確認
モックまたはスパイが使用されると、特定の相互作用が発生したことをverify
できます。 文字通り、「ねえ、Mockito、このメソッドがこれらの引数で呼び出されたことを確認してください」と言っています。
次の人為的な例を考えてみましょう。
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); when(passwordEncoder.encode("a")).thenReturn("1"); passwordEncoder.encode("a"); verify(passwordEncoder).encode("a");
ここでは、モックを設定し、そのencode()
メソッドを呼び出しました。 最後の行は、モックのencode()
メソッドが特定の引数値a
で呼び出されたことを確認します。 スタブ呼び出しの検証は冗長であることに注意してください。 前のスニペットの目的は、いくつかの相互作用が発生した後に検証を行うという考えを示すことです。
最後の行を別の引数(たとえばb
)に変更すると、前のテストは失敗し、Mockitoは実際の呼び出しに別の引数(期待されるa
ではなくb
)があると文句を言います。
引数マッチャーは、スタブの場合と同じように検証に使用できます。
verify(passwordEncoder).encode(anyString());
デフォルトでは、Mockitoはメソッドが一度呼び出されたことを確認しますが、呼び出しをいくつでも確認できます。
// verify the exact number of invocations verify(passwordEncoder, times(42)).encode(anyString()); // verify that there was at least one invocation verify(passwordEncoder, atLeastOnce()).encode(anyString()); // verify that there were at least five invocations verify(passwordEncoder, atLeast(5)).encode(anyString()); // verify the maximum number of invocations verify(passwordEncoder, atMost(5)).encode(anyString()); // verify that it was the only invocation and // that there're no more unverified interactions verify(passwordEncoder, only()).encode(anyString()); // verify that there were no invocations verify(passwordEncoder, never()).encode(anyString());
まれにしか使用されないverify()
の機能は、タイムアウト時に失敗する機能です。これは、主に並行コードのテストに役立ちます。 たとえば、パスワードエンコーダーがverify()
と同時に別のスレッドで呼び出された場合、次のようにテストを記述できます。

usePasswordEncoderInOtherThread(); verify(passwordEncoder, timeout(500)).encode("a");
このテストは、 encode()
が呼び出され、500ミリ秒以内に終了した場合に成功します。 指定した全期間待機する必要がある場合は、 timeout()
)の代わりにafter()
を使用してください。
verify(passwordEncoder, after(500)).encode("a");
他の検証モード( times()
、 atLeast()
など)をtimeout()
およびafter()
)と組み合わせて、より複雑なテストを行うことができます。
// passes as soon as encode() has been called 3 times within 500 ms verify(passwordEncoder, timeout(500).times(3)).encode("a");
times()
の他に、サポートされている検証モードには、 only()
、 atLeast()
、およびatLeastOnce()
( atLeast(1)
のエイリアスとして)が含まれます。
Mockitoでは、モックのグループで呼び出し順序を確認することもできます。 あまり頻繁に使用される機能ではありませんが、呼び出しの順序が重要な場合に役立つことがあります。 次の例を考えてみましょう。
PasswordEncoder first = mock(PasswordEncoder.class); PasswordEncoder second = mock(PasswordEncoder.class); // simulate calls first.encode("f1"); second.encode("s1"); first.encode("f2"); // verify call order InOrder inOrder = inOrder(first, second); inOrder.verify(first).encode("f1"); inOrder.verify(second).encode("s1"); inOrder.verify(first).encode("f2");
シミュレートされた呼び出しの順序を並べ替えると、 VerificationInOrderFailure
でテストが失敗します。
呼び出しがないことは、 verifyZeroInteractions()
を使用して確認することもできます。 このメソッドは、1つまたは複数のモックを引数として受け入れ、渡されたモックのメソッドが呼び出された場合は失敗します。
また、 verifyNoMoreInteractions()
メソッドについても言及する価値があります。これは、モックを引数として取り、それらのモックに対するすべての呼び出しが検証されたことを確認するために使用できるためです。
引数のキャプチャ
メソッドが特定の引数で呼び出されたことを確認するだけでなく、Mockitoを使用すると、それらの引数をキャプチャして、後でカスタムアサーションを実行できるようになります。 つまり、「ねえ、Mockito、このメソッドが呼び出されたことを確認し、呼び出された引数の値を教えてください」と言っているのです。
PasswordEncoder
のモックを作成し、 encode()
を呼び出し、引数をキャプチャして、その値を確認しましょう。
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); passwordEncoder.encode("password"); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder).encode(passwordCaptor.capture()); assertEquals("password", passwordCaptor.getValue());
ご覧のとおり、検証のために、 encode()
の引数としてpasswordCaptor.capture()
を渡します。 これにより、引数を保存する引数マッチャーが内部的に作成されます。 次に、 passwordCaptor.getValue()
を使用してキャプチャされた値を取得し、 assertEquals()
を使用して検査します。
複数の呼び出しにわたって引数をキャプチャする必要がある場合、 ArgumentCaptor
を使用すると、次のようにgetAllValues()
を使用してすべての値を取得できます。
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); passwordEncoder.encode("password1"); passwordEncoder.encode("password2"); passwordEncoder.encode("password3"); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder, times(3)).encode(passwordCaptor.capture()); assertEquals(Arrays.asList("password1", "password2", "password3"), passwordCaptor.getAllValues());
同じ手法を使用して、可変アリティメソッド引数(varargsとも呼ばれます)をキャプチャできます。
簡単な例のテスト
Mockitoについて詳しく知ったところで、デモに戻りましょう。 isValidUser
メソッドテストを書いてみましょう。 これがどのように見えるかです:
public class UserServiceTest { private static final String PASSWORD = "password"; private static final User ENABLED_USER = new User("user id", "hash", true); private static final User DISABLED_USER = new User("disabled user id", "disabled user password hash", false); private UserRepository userRepository; private PasswordEncoder passwordEncoder; private UserService userService; @Before public void setup() { userRepository = createUserRepository(); passwordEncoder = createPasswordEncoder(); userService = new UserService(userRepository, passwordEncoder); } @Test public void shouldBeValidForValidCredentials() { boolean userIsValid = userService.isValidUser(ENABLED_USER.getId(), PASSWORD); assertTrue(userIsValid); // userRepository had to be used to find a user with verify(userRepository).findById(ENABLED_USER.getId()); // passwordEncoder had to be used to compute a hash of "password" verify(passwordEncoder).encode(PASSWORD); } @Test public void shouldBeInvalidForInvalidId() { boolean userIsValid = userService.isValidUser("invalid id", PASSWORD); assertFalse(userIsValid); InOrder inOrder = inOrder(userRepository, passwordEncoder); inOrder.verify(userRepository).findById("invalid id"); inOrder.verify(passwordEncoder, never()).encode(anyString()); } @Test public void shouldBeInvalidForInvalidPassword() { boolean userIsValid = userService.isValidUser(ENABLED_USER.getId(), "invalid"); assertFalse(userIsValid); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder).encode(passwordCaptor.capture()); assertEquals("invalid", passwordCaptor.getValue()); } @Test public void shouldBeInvalidForDisabledUser() { boolean userIsValid = userService.isValidUser(DISABLED_USER.getId(), PASSWORD); assertFalse(userIsValid); verify(userRepository).findById(DISABLED_USER.getId()); verifyZeroInteractions(passwordEncoder); } private PasswordEncoder createPasswordEncoder() { PasswordEncoder mock = mock(PasswordEncoder.class); when(mock.encode(anyString())).thenReturn("any password hash"); when(mock.encode(PASSWORD)).thenReturn(ENABLED_USER.getPasswordHash()); return mock; } private UserRepository createUserRepository() { UserRepository mock = mock(UserRepository.class); when(mock.findById(ENABLED_USER.getId())).thenReturn(ENABLED_USER); when(mock.findById(DISABLED_USER.getId())).thenReturn(DISABLED_USER); return mock; } }
APIの下に飛び込む
Mockitoは読みやすく便利なAPIを提供しますが、その制限を理解し、奇妙なエラーを回避するために、その内部動作のいくつかを調べてみましょう。
次のスニペットが実行されたときにMockito内で何が起こっているかを調べてみましょう。
// 1: create PasswordEncoder mock = mock(PasswordEncoder.class); // 2: stub when(mock.encode("a")).thenReturn("1"); // 3: act mock.encode("a"); // 4: verify verify(mock).encode(or(eq("a"), endsWith("b")));
明らかに、最初の行はモックを作成します。 Mockitoは、ByteBuddyを使用して、指定されたクラスのサブクラスを作成します。 新しいクラスオブジェクトの名前はdemo.mockito.PasswordEncoder$MockitoMock$1953422997
のように生成され、 equals()
はIDのチェックとして機能し、 hashCode()
はIDハッシュコードを返します。 クラスが生成されてロードされると、そのインスタンスはObjenesisを使用して作成されます。
次の行を見てみましょう:
when(mock.encode("a")).thenReturn("1");
順序は重要です。ここで実行される最初のステートメントはmock.encode("a")
です。これは、デフォルトの戻り値null
でモックに対してencode()
を呼び出します。 つまり、実際には、 when()
の引数としてnull
を渡しています。 Mockitoは、モックされたメソッドの呼び出しに関する情報を、そのメソッドが呼び出されたときにいわゆる「進行中のスタブ」に格納するため、 when()
に渡される正確な値を気にしません。 後で、when when()
を呼び出すと、Mockitoはその進行中のスタブオブジェクトをプルし、 when()
の結果として返します。 次に、返された進行中のスタブオブジェクトに対してthenReturn(“1”)
を呼び出します。
3行目、 mock.encode("a");
単純です。スタブメソッドを呼び出しています。 内部的には、Mockitoはこの呼び出しを保存してさらに検証し、スタブされた呼び出しの回答を返します。 私たちの場合、それは文字列1
です。
4行目( verify(mock).encode(or(eq("a"), endsWith("b")));
)では、Mockitoにencode()
の呼び出しがあったことを確認するように求めています。特定の引数。
最初にverify()
が実行され、Mockitoの内部状態が検証モードになります。 Mockitoがその状態をThreadLocal
に保持することを理解することが重要です。 これにより、優れた構文を実装できますが、一方で、フレームワークが不適切に使用された場合(たとえば、検証やスタブ以外で引数マッチャーを使用しようとした場合)、奇妙な動作が発生する可能性があります。
では、Mockitoはどのようにしてor
マッチャーを作成しますか? 最初に、 eq("a")
が呼び出され、 equals
マッチャーがマッチャースタックに追加されます。 次に、 endsWith("b")
が呼び出され、 endsWith
マッチャーがスタックに追加されます。 最後に、 or(null, null)
が呼び出されます。スタックからポップした2つのマッチャーを使用して、 or
マッチャーを作成し、それをスタックにプッシュします。 最後に、 encode()
が呼び出されます。 次に、Mockitoは、メソッドが予想される回数、予想される引数で呼び出されたことを確認します。
引数マッチャーは変数に抽出できませんが(呼び出し順序が変更されるため)、メソッドに抽出できます。 これにより、呼び出し順序が保持され、スタックが正しい状態に保たれます。
verify(mock).encode(matchCondition()); … String matchCondition() { return or(eq("a"), endsWith("b")); }
デフォルトの回答の変更
前のセクションでは、モックされたメソッドが呼び出されたときに「空の」値を返すようにモックを作成しました。 この動作は構成可能です。 org.mockito.stubbing.Answer
によって提供されるものが適切でない場合は、単体テストが複雑になりすぎると何かが間違っていることを示している可能性があります。 KISSの原則を忘れないでください!
Mockitoが提供する事前定義されたデフォルトの回答を調べてみましょう。
RETURNS_DEFAULTS
はデフォルトの戦略です。 モックを設定するときに明示的に言及する価値はありません。CALLS_REAL_METHODS
は、スタブされていない呼び出しで実際のメソッドを呼び出します。RETURNS_SMART_NULLS
は、スタブされていないメソッド呼び出しによって返されたオブジェクトを使用するときに、null
ではなくSmartNull
を返すことにより、NullPointerException
を回避します。 それでもNullPointerException
で失敗しますが、SmartNull
は、スタブされていないメソッドが呼び出された行で、より適切なスタックトレースを提供します。 これにより、RETURNS_SMART_NULLSをRETURNS_SMART_NULLS
のデフォルトの回答にする価値があります。RETURNS_MOCKS
は、最初に通常の「空の」値を返そうとし、次に可能であればモック、それ以外の場合はnull
を返そうとします。 空の基準は、以前に見たものとは少し異なります。文字列と配列に対してnull
を返す代わりに、RETURNS_MOCKS
で作成されたモックはそれぞれ空の文字列と空の配列を返します。RETURNS_SELF
は、ビルダーをモックするのに役立ちます。 この設定では、モックされたクラスのクラス(またはスーパークラス)と等しいタイプの何かを返すメソッドが呼び出された場合、モックはそれ自体のインスタンスを返します。RETURNS_DEEP_STUBS
はRETURNS_MOCKSよりも深くなり、モックからモックを返すことができるモックを作成しますRETURNS_MOCKS
とは対照的に、RETURNS_MOCKS
では空のルールがデフォルトでRETURNS_DEEP_STUBS
ため、文字列と配列に対してnull
を返します。
interface We { Are we(); } interface Are { So are(); } interface So { Deep so(); } interface Deep { boolean deep(); } ... We mock = mock(We.class, Mockito.RETURNS_DEEP_STUBS); when(mock.we().are().so().deep()).thenReturn(true); assertTrue(mock.we().are().so().deep());
モックに名前を付ける
Mockitoを使用すると、モックに名前を付けることができます。これは、テストに多数のモックがあり、それらを区別する必要がある場合に便利な機能です。 とはいえ、モックに名前を付ける必要があるのは、デザインが悪いことの兆候かもしれません。 次のことを考慮してください。
PasswordEncoder robustPasswordEncoder = mock(PasswordEncoder.class); PasswordEncoder weakPasswordEncoder = mock(PasswordEncoder.class); verify(robustPasswordEncoder).encode(anyString());
Mockitoは文句を言いますが、正式にモックに名前を付けていないため、どれがどれかわかりません。
Wanted but not invoked: passwordEncoder.encode(<any string>);
構築時に文字列を渡して名前を付けましょう。
PasswordEncoder robustPasswordEncoder = mock(PasswordEncoder.class, "robustPasswordEncoder"); PasswordEncoder weakPasswordEncoder = mock(PasswordEncoder.class, "weakPasswordEncoder"); verify(robustPasswordEncoder).encode(anyString());
これで、エラーメッセージがわかりやすくなり、 robustPasswordEncoder
を明確に示します。
Wanted but not invoked: robustPasswordEncoder.encode(<any string>);
複数のモックインターフェースの実装
場合によっては、複数のインターフェースを実装するモックを作成したいことがあります。 Mockitoは、次のように簡単にそれを行うことができます。
PasswordEncoder mock = mock( PasswordEncoder.class, withSettings().extraInterfaces(List.class, Map.class)); assertTrue(mock instanceof List); assertTrue(mock instanceof Map);
Listening Invocations
A mock can be configured to call an invocation listener every time a method of the mock was called. Inside the listener, you can find out whether the invocation produced a value or if an exception was thrown.
InvocationListener invocationListener = new InvocationListener() { @Override public void reportInvocation(MethodInvocationReport report) { if (report.threwException()) { Throwable throwable = report.getThrowable(); // do something with throwable throwable.printStackTrace(); } else { Object returnedValue = report.getReturnedValue(); // do something with returnedValue System.out.println(returnedValue); } } }; PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().invocationListeners(invocationListener)); passwordEncoder.encode("1");
In this example, we're dumping either the returned value or a stack trace to a system output stream. Our implementation does roughly the same as Mockito's org.mockito.internal.debugging.VerboseMockInvocationLogger
(don't use this directly, it's internal stuff). If logging invocations is the only feature you need from the listener, then Mockito provides a cleaner way to express your intent with the verboseLogging()
setting:
PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().verboseLogging());
Take notice, though, that Mockito will call the listeners even when you're stubbing methods. 次の例を考えてみましょう。
PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().verboseLogging()); // listeners are called upon encode() invocation when(passwordEncoder.encode("1")).thenReturn("encoded1"); passwordEncoder.encode("1"); passwordEncoder.encode("2");
This snippet will produce an output similar to the following:
############ Logging method invocation #1 on mock/spy ######## passwordEncoder.encode("1"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:85) has returned: "null" ############ Logging method invocation #2 on mock/spy ######## stubbed: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:85) passwordEncoder.encode("1"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:89) has returned: "encoded1" (java.lang.String) ############ Logging method invocation #3 on mock/spy ######## passwordEncoder.encode("2"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:90) has returned: "null"
Note that the first logged invocation corresponds to calling encode()
while stubbing it. It's the next invocation that corresponds to calling the stubbed method.
その他の設定
Mockito offers a few more settings that let you do the following:
- Enable mock serialization by using
withSettings().serializable()
. - Turn off recording of method invocations to save memory (this will make verification impossible) by using
withSettings().stubOnly()
. - Use the constructor of a mock when creating its instance by using
withSettings().useConstructor()
. When mocking inner non-static classes, add anouterInstance()
setting, like so:withSettings().useConstructor().outerInstance(outerObject)
.
If you need to create a spy with custom settings (such as a custom name), there's a spiedInstance()
setting, so that Mockito will create a spy on the instance you provide, like so:
UserService userService = new UserService( mock(UserRepository.class), mock(PasswordEncoder.class)); UserService userServiceMock = mock( UserService.class, withSettings().spiedInstance(userService).name("coolService"));
When a spied instance is specified, Mockito will create a new instance and populate its non-static fields with values from the original object. That's why it's important to use the returned instance: Only its method calls can be stubbed and verified.
Note that, when you create a spy, you're basically creating a mock that calls real methods:
// creating a spy this way... spy(userService); // ... is a shorthand for mock(UserService.class, withSettings() .spiedInstance(userService) .defaultAnswer(CALLS_REAL_METHODS));
When Mockito Tastes Bad
It's our bad habits that make our tests complex and unmaintainable, not Mockito. For example, you may feel the need to mock everything. This kind of thinking leads to testing mocks instead of production code. Mocking third-party APIs can also be dangerous due to potential changes in that API that can break the tests.
Though bad taste is a matter of perception, Mockito provides a few controversial features that can make your tests less maintainable. Sometimes stubbing isn't trivial, or an abuse of dependency injection can make recreating mocks for each test difficult, unreasonable or inefficient.
Clearing Invocations
Mockito allows for clearing invocations for mocks while preserving stubbing, like so:
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); UserRepository userRepository = mock(UserRepository.class); // use mocks passwordEncoder.encode(null); userRepository.findById(null); // clear clearInvocations(passwordEncoder, userRepository); // succeeds because invocations were cleared verifyZeroInteractions(passwordEncoder, userRepository);
Resort to clearing invocations only if recreating a mock would lead to significant overhead or if a configured mock is provided by a dependency injection framework and stubbing is non-trivial.
Resetting a Mock
Resetting a mock with reset()
is another controversial feature and should be used in extremely rare cases, like when a mock is injected by a container and you can't recreate it for each test.
Overusing Verify
Another bad habit is trying to replace every assert with Mockito's verify()
. It's important to clearly understand what is being tested: interactions between collaborators can be checked with verify()
, while confirming the observable results of an executed action is done with asserts.
Mockito Is about Frame of Mind
Using Mockito is not just a matter of adding another dependency, it requires changing how you think about your unit tests while removing a lot of boilerplate.
With multiple mock interfaces, listening invocations, matchers and argument captors, we've seen how Mockito makes your tests cleaner and easier to understand, but like any tool, it must be used appropriately to be useful. Now armed with the knowledge of Mockito's inner workings, you can take your unit testing to the next level.