アジャイルの時代にはユニットテストが必須になり、自動テストに役立つツールがたくさんあります。そのようなツールの1つがMockitoです。これは、テスト用のモックオブジェクトを作成および構成できるオープンソースフレームワークです。
この記事では、モックの作成と構成、およびそれらを使用してテスト対象のシステムの予想される動作を検証する方法について説明します。また、Mockitoの内部について少し詳しく説明し、その設計と注意事項をよりよく理解します。使用します JUnit ユニットテストフレームワークとしてですが、MockitoはJUnitにバインドされていないため、別のフレームワークを使用している場合でも従うことができます。
最近、Mockitoを入手するのは簡単です。 Gradleを使用している場合は、ビルドスクリプトに次の1行を追加するだけです。
testCompile 'org.mockito:mockito−core:2.7.7'
まだMavenを好む私のような人は、次のように依存関係にMockitoを追加するだけです。
org.mockito mockito-core 2.7.7 test
もちろん、世界はMavenやGradleよりもはるかに広いです。プロジェクト管理ツールを使用して、Mockitojarアーティファクトをから自由にフェッチできます。 Maven中央リポジトリ 。
単体テストは、依存関係の動作に依存することなく、特定のクラスまたはメソッドの動作をテストするように設計されています。コードの最小の「ユニット」をテストしているため、これらの依存関係の実際の実装を使用する必要はありません。さらに、さまざまな動作をテストするときに、これらの依存関係のわずかに異なる実装を使用します。これに対する従来のよく知られたアプローチは、「スタブ」を作成することです。これは、特定のシナリオに適したインターフェイスの特定の実装です。このような実装には通常、ハードコードされたロジックがあります。スタブは一種のテストダブルです。他の種類には、偽物、モック、スパイ、ダミーなどが含まれます。
Mockitoで頻繁に使用されているため、「モック」と「スパイ」の2種類のテストダブルのみに焦点を当てます。
モックとは何ですか?明らかに、それはあなたが仲間の開発者をからかう場所ではありません。単体テストのモックは、制御された方法で実際のサブシステムの動作を実装するオブジェクトを作成する場合です。つまり、依存関係の代わりにモックが使用されます。
Mockitoを使用して、モックを作成し、特定のメソッドが呼び出されたときに何をするかをMockitoに指示してから、テストで本物の代わりにモックインスタンスを使用します。テスト後、モックにクエリを実行して、呼び出された特定のメソッドを確認したり、状態の変更という形で副作用を確認したりできます。
デフォルトでは、Mockitoはモックのすべてのメソッドの実装を提供します。
スパイは、Mockitoが作成するもう1つのタイプのテストダブルです。モックとは対照的に、スパイを作成するには、スパイするインスタンスが必要です。デフォルトでは、スパイはすべてのメソッド呼び出しを実際のオブジェクトに委任し、呼び出されたメソッドとパラメーターを記録します。それがそれをスパイにしているのです。それは実際の物体をスパイしているのです。
可能な限り、スパイの代わりにモックを使用することを検討してください。スパイは、簡単にテストできるように再設計できないレガシーコードのテストに役立つ場合がありますが、スパイを使用してクラスを部分的にモックする必要があることは、クラスのパフォーマンスが高すぎることを示しているため、単一責任の原則に違反しています。
テストを書くことができる簡単なデモを見てみましょう。 UserRepository
があるとします。識別子によってユーザーを見つけるための単一のメソッドとのインターフェース。また、クリアテキストのパスワードをパスワードハッシュに変換するパスワードエンコーダーの概念もあります。両方UserRepository
およびPasswordEncoder
UserService
の依存関係(コラボレーターとも呼ばれます)ですコンストラクターを介して注入されます。デモコードは次のようになります。
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; } ... }
public interface PasswordEncoder { String encode(String password); }
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.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 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');
このスタブ方法のスニペットは次のようになります。「Return a
passwordEncoder
のencode()
のときメソッドは1
の引数で呼び出されます。」
最初の方法は、タイプセーフであり、読みやすいため、推奨されると考えられています。ただし、スパイの実際のメソッドを呼び出すと望ましくない副作用が発生する可能性があるため、スパイの実際のメソッドをスタブする場合など、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()
を提供しますおよび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は例外インスタンスを検証するのと同じ方法で例外クラスを検証することはできないため、規律を守り、不正なクラスオブジェクトを渡さないようにする必要があります。たとえば、次のようにするとIOException
がスローされます。しかしencode()
チェックされた例外をスローすることは期待されていません:
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'));
encode()
にどの値を渡しても、結果は同じであることがわかります。 anyString()
を使用したためその最初の行の引数マッチャー。その行を平易な英語で書き直すと、「パスワードエンコーダーが任意の文字列をエンコードするように求められたら、文字列「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);
エラーを修正するには、最後の行を置き換えてeq
を含める必要があります次のように、a
の引数マッチャー。
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()
または任意-何でも;呼び出しをスタブするときは、正確な値が必要です。
Mockitoでまだ利用できないマッチングロジックを提供する必要がある場合は、カスタムマッチャーが役に立ちます。引数を重要な方法で照合する必要があるということは、設計に問題があるか、テストが複雑になりすぎていることを示しているため、カスタムマッチャーを作成するという決定は軽視すべきではありません。
そのため、isNull()
などの寛大な引数マッチャーを使用してテストを簡略化できるかどうかを確認する価値があります。およびnullable()
カスタムマッチャーを作成する前に。それでも引数マッチャーを作成する必要があると感じる場合は、Mockitoがそれを行うための一連のメソッドを提供します。
次の例を考えてみましょう。
FileFilter fileFilter = mock(FileFilter.class); ArgumentMatcher 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()
マッチャーをモックされたメソッドへの引数として渡し、スタブしてtrue
を返します。ファイル名が「運」で終わる場合。あなたはArgumentMatcher
を扱うことができます関数型インターフェースとして、ラムダを使用してインスタンスを作成します(これは例で行ったことです)。簡潔でない構文は次のようになります。
ビジネスインテリジェンスと人工知能
ArgumentMatcher hasLuck = new ArgumentMatcher() { @Override public boolean matches(File file) { return file.getName().endsWith('luck'); } };
プリミティブ型で機能する引数マッチャーを作成する必要がある場合は、org.mockito.ArgumentMatchers
に他のいくつかのメソッドがあります。
条件が複雑すぎて基本的なマッチャーで処理できない場合は、カスタム引数マッチャーを作成する価値があるとは限りません。時々マッチャーを組み合わせることでうまくいくでしょう。 Mockitoは、プリミティブ型と非プリミティブ型の両方に一致する引数マッチャーに一般的な論理演算(「not」、「and」、「or」)を実装するための引数マッチャーを提供します。これらのマッチャーは、静的メソッドとして実装されています。 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」または 含む 'に'。
にリストされているあまり一般的でないマッチャーがあることに注意してください org.mockito.AdditionalMatchers
geq()
、leq()
、gt()
などのクラスおよびlt()
は、プリミティブ値および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は実際の呼び出しに別の引数(予想される| _ + _ではなくb
)があると文句を言います。 |)。
引数マッチャーは、スタブの場合と同じように検証に使用できます。
a
デフォルトでは、Mockitoはメソッドが一度呼び出されたことを確認しますが、呼び出しをいくつでも確認できます。
verify(passwordEncoder).encode(anyString());
// 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');
の場合、このテストは成功しますが呼び出され、500ミリ秒以内に終了します。指定した全期間待機する必要がある場合は、encode()
を使用してくださいafter()
の代わりに:
timeout()
他の検証モード(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()
のエイリアスとして)。
Mockitoでは、モックのグループで呼び出し順序を確認することもできます。あまり頻繁に使用される機能ではありませんが、呼び出しの順序が重要な場合に役立つことがあります。次の例を考えてみましょう。
atLeast(1)
シミュレートされた呼び出しの順序を並べ替えると、テストは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
を使用して確認することもできます。このメソッドは、1つまたは複数のモックを引数として受け入れ、渡されたモックのメソッドが呼び出された場合は失敗します。
verifyZeroInteractions()
についても言及する価値がありますメソッド。モックを引数として取り、それらのモックに対するすべての呼び出しが検証されたことを確認するために使用できます。
Mockitoでは、メソッドが特定の引数で呼び出されたことを確認するだけでなく、それらの引数をキャプチャして、後でカスタムアサーションを実行できるようにすることができます。つまり、「ねえ、Mockito、このメソッドが呼び出されたことを確認し、呼び出された引数の値を教えてください」と言っているのです。
verifyNoMoreInteractions()
のモックを作成し、PasswordEncoder
を呼び出し、引数をキャプチャして、その値を確認しましょう。
encode()
ご覧のとおり、PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); passwordEncoder.encode('password'); ArgumentCaptor passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder).encode(passwordCaptor.capture()); assertEquals('password', passwordCaptor.getValue());
を渡しますpasswordCaptor.capture()
の引数として検証用。これにより、引数を保存する引数マッチャーが内部的に作成されます。次に、キャプチャした値をencode()
で取得します。 passwordCaptor.getValue()
で調べます。
複数の呼び出しにわたって引数をキャプチャする必要がある場合、assertEquals()
次のように、ArgumentCaptor
を使用してすべての値を取得できます。
getAllValues()
同じ手法を使用して、可変アリティメソッドの引数(可変引数とも呼ばれます)をキャプチャできます。
Mockitoについて詳しく理解できたので、デモに戻りましょう。 PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); passwordEncoder.encode('password1'); passwordEncoder.encode('password2'); passwordEncoder.encode('password3'); ArgumentCaptor passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder, times(3)).encode(passwordCaptor.capture()); assertEquals(Arrays.asList('password1', 'password2', 'password3'), passwordCaptor.getAllValues());
を書いてみましょうメソッドテスト。外観は次のとおりです。
isValidUser
Mockitoは読みやすく便利なAPIを提供しますが、その制限を理解し、奇妙なエラーを回避するために、その内部動作のいくつかを調べてみましょう。
次のスニペットが実行されたときにMockito内で何が起こっているかを調べてみましょう。
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 id='user id' 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 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; } }
明らかに、最初の行はモックを作成します。 Mockitoは ByteBuddy 指定されたクラスのサブクラスを作成します。新しいクラスオブジェクトには、// 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')));
のような生成された名前があります。そのdemo.mockito.PasswordEncoder$MockitoMock53422997
IDのチェックとして機能し、equals()
IDハッシュコードを返します。クラスが生成されてロードされると、そのインスタンスはを使用して作成されます オブジェネシス 。
次の行を見てみましょう:
hashCode()
順序は重要です。ここで実行される最初のステートメントはwhen(mock.encode('a')).thenReturn('1');
であり、mock.encode('a')
を呼び出します。デフォルトの戻り値がencode()
のモック。本当に、私たちは合格していますnull
null
の引数として。 Mockitoは、どの正確な値がwhen()
に渡されるかを気にしませんこれは、モックされたメソッドの呼び出しに関する情報が、そのメソッドが呼び出されたときに、いわゆる「進行中のスタブ」に格納されるためです。後でwhen()
を呼び出すと、Mockitoはその進行中のスタブオブジェクトをプルし、when()
の結果として返します。次に、when()
と呼びます。返された進行中のスタブオブジェクト。
3行目thenReturn(“1”)
シンプルです。スタブメソッドを呼び出しています。内部的には、Mockitoはこの呼び出しを保存してさらに検証し、スタブされた呼び出しの回答を返します。私たちの場合、それは文字列mock.encode('a');
です。
4行目(1
)では、Mockitoにverify(mock).encode(or(eq('a'), endsWith('b')));
の呼び出しがあったことを確認するように求めています。それらの特定の議論で。
encode()
最初に実行され、Mockitoの内部状態が検証モードに変わります。 Mockitoはその状態をverify()
に保つことを理解することが重要です。これにより、優れた構文を実装できますが、一方で、フレームワークが不適切に使用された場合(検証やスタブ以外で引数マッチャーを使用しようとした場合など)、奇妙な動作が発生する可能性があります。
では、MockitoはどのようにしてThreadLocal
を作成しますかマッチャー?まず、or
が呼び出され、eq('a')
マッチャーがマッチャースタックに追加されます。次に、equals
が呼び出され、endsWith('b')
マッチャーがスタックに追加されます。ついにendsWith
と呼ばれます—スタックからポップした2つのマッチャーを使用して、or(null, null)
を作成します。マッチャー、そしてそれをスタックにプッシュします。最後に、or
と呼ばれます。次に、Mockitoは、メソッドが予想される回数、予想される引数で呼び出されたことを確認します。
引数マッチャーは変数に抽出できませんが(呼び出し順序が変更されるため)、メソッドに抽出できます。これにより、呼び出し順序が保持され、スタックが正しい状態に保たれます。
encode()
前のセクションでは、モックされたメソッドが呼び出されたときに「空の」値を返すようにモックを作成しました。この動作は構成可能です。 verify(mock).encode(matchCondition()); … String matchCondition() { return or(eq('a'), endsWith('b')); }
の独自の実装を提供することもできますMockitoが提供するものが適切でない場合でも、単体テストが複雑になりすぎると、問題が発生している可能性があります。 KISSの原則を忘れないでください!
Mockitoが提供する事前定義されたデフォルトの回答を調べてみましょう。
org.mockito.stubbing.Answer
デフォルトの戦略です。モックを設定するときに明示的に言及する価値はありません。
RETURNS_DEFAULTS
スタブされていない呼び出しが実際のメソッドを呼び出すようにします。
CALLS_REAL_METHODS
RETURNS_SMART_NULLS
を回避しますNullPointerException
を返すことによってSmartNull
の代わりにスタブされていないメソッド呼び出しによって返されたオブジェクトを使用する場合。それでもnull
で失敗しますが、NullPointerException
unstubbedメソッドが呼び出された行でより良いスタックトレースが得られます。これにより、SmartNull
を持つ価値があります。 Mockitoのデフォルトの答えになります!
RETURNS_SMART_NULLS
最初に通常の「空の」値を返そうとし、次に可能であればモックを返し、RETURNS_MOCKS
さもないと。空の基準は、以前に見たものとは少し異なります。null
を返す代わりに文字列と配列の場合、null
で作成されたモック空の文字列と空の配列をそれぞれ返します。
RETURNS_MOCKS
ビルダーをあざけるのに便利です。この設定では、モッククラスのクラス(またはスーパークラス)と等しいタイプの何かを返すメソッドが呼び出された場合、モックはそれ自体のインスタンスを返します。
機能ドキュメントは、
RETURNS_SELF
RETURNS_DEEP_STUBS
よりも深くなりますモックからモックを返すことができるモックを作成します。RETURNS_MOCKS
とは対照的に、空のルールはRETURNS_MOCKS
でデフォルトであるため、RETURNS_DEEP_STUBS
を返します。文字列と配列の場合:
null
Mockitoを使用すると、モックに名前を付けることができます。これは、テストに多数のモックがあり、それらを区別する必要がある場合に便利な機能です。とはいえ、モックに名前を付ける必要があるのは、デザインが悪いことの兆候かもしれません。次のことを考慮してください。
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());
構築時に文字列を渡して名前を付けましょう。
Wanted but not invoked: passwordEncoder.encode();
これで、エラーメッセージがわかりやすくなり、PasswordEncoder robustPasswordEncoder = mock(PasswordEncoder.class, 'robustPasswordEncoder'); PasswordEncoder weakPasswordEncoder = mock(PasswordEncoder.class, 'weakPasswordEncoder'); verify(robustPasswordEncoder).encode(anyString());
を明確に示します。
robustPasswordEncoder
場合によっては、複数のインターフェースを実装するモックを作成したいことがあります。 Mockitoは、次のように簡単にそれを行うことができます。
Wanted but not invoked: robustPasswordEncoder.encode();
モックは、モックのメソッドが呼び出されるたびに呼び出しリスナーを呼び出すように構成できます。リスナー内で、呼び出しによって値が生成されたかどうか、または例外がスローされたかどうかを確認できます。
PasswordEncoder mock = mock( PasswordEncoder.class, withSettings().extraInterfaces(List.class, Map.class)); assertTrue(mock instanceof List); assertTrue(mock instanceof Map);
この例では、戻り値またはスタックトレースのいずれかをシステム出力ストリームにダンプしています。私たちの実装は、Mockitoの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');
とほぼ同じです。 (これを直接使用しないでください。内部のものです)。呼び出しをログに記録することがリスナーから必要な唯一の機能である場合、Mockitoはorg.mockito.internal.debugging.VerboseMockInvocationLogger
で意図を表現するためのよりクリーンな方法を提供します。設定:
verboseLogging()
ただし、メソッドをスタブしている場合でも、Mockitoがリスナーを呼び出すことに注意してください。次の例を考えてみましょう。
PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().verboseLogging());
このスニペットは、次のような出力を生成します。
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');
ログに記録された最初の呼び出しは、############ 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'
の呼び出しに対応することに注意してください。それをスタブしながら。これは、スタブメソッドの呼び出しに対応する次の呼び出しです。
Mockitoには、次のことを可能にするいくつかの設定があります。
encode()
を使用してモックシリアル化を有効にします。withSettings().serializable()
を使用して、メモリを節約するためにメソッド呼び出しの記録をオフにします(これにより検証が不可能になります)。withSettings().stubOnly()
を使用してインスタンスを作成するときは、モックのコンストラクターを使用します。内部の非静的クラスをモックする場合は、withSettings().useConstructor()
を追加します。設定、そのように:outerInstance()
。カスタム設定(カスタム名など)でスパイを作成する必要がある場合は、withSettings().useConstructor().outerInstance(outerObject)
があります。次のように、Mockitoが指定したインスタンスにスパイを作成するように設定します。
spiedInstance()
スパイされたインスタンスが指定されると、Mockitoは新しいインスタンスを作成し、その非静的フィールドに元のオブジェクトの値を入力します。そのため、返されたインスタンスを使用することが重要です。スタブ化して検証できるのは、そのメソッド呼び出しのみです。
スパイを作成するときは、基本的に実際のメソッドを呼び出すモックを作成していることに注意してください。
UserService userService = new UserService( mock(UserRepository.class), mock(PasswordEncoder.class)); UserService userServiceMock = mock( UserService.class, withSettings().spiedInstance(userService).name('coolService'));
Mockitoではなく、テストを複雑で保守不可能にするのは私たちの悪い習慣です。たとえば、すべてをモックする必要があると感じるかもしれません。この種の考え方は、本番コードではなくモックのテストにつながります。サードパーティのAPIをモックすることも、そのAPIに変更が加えられてテストが失敗する可能性があるため、危険な場合があります。
味の悪さは知覚の問題ですが、Mockitoは、テストの保守性を低下させる可能性のあるいくつかの物議を醸す機能を提供します。スタブが簡単ではない場合や、依存性注入の乱用により、各テストのモックの再作成が困難、不合理、または非効率になる場合があります。
Mockitoを使用すると、次のように、スタブを保持しながらモックの呼び出しをクリアできます。
// creating a spy this way... spy(userService); // ... is a shorthand for mock(UserService.class, withSettings() .spiedInstance(userService) .defaultAnswer(CALLS_REAL_METHODS));
モックを再作成すると大きなオーバーヘッドが発生する場合、または構成されたモックが依存性注入フレームワークによって提供され、スタブが重要である場合にのみ、呼び出しをクリアすることに頼ってください。
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);
でモックをリセットするこれは別の物議を醸す機能であり、モックがコンテナによって注入され、テストごとにモックを再作成できない場合など、非常にまれなケースで使用する必要があります。
もう1つの悪い習慣は、すべてのアサーションをMockitoのreset()
に置き換えようとすることです。テスト対象を明確に理解することが重要です。実行されたアクションの観察可能な結果がアサートで行われることを確認しながら、共同作業者間の相互作用をverify()
で確認できます。
Mockitoを使用するには、別の依存関係を追加するだけでなく、多くの定型文を削除しながら、単体テストについての考え方を変える必要があります。
複数のモックインターフェース、リスニング呼び出し、マッチャー、引数キャプターを使用して、Mockitoがテストをよりクリーンで理解しやすくする方法を見てきましたが、他のツールと同様に、有用であるためには適切に使用する必要があります。 Mockitoの内部動作の知識を身に付けたので、ユニットテストを次のレベルに引き上げることができます。