メソッドの途中で計算された値を、他のメソッドの引数として使うというケースをテストすることがままあります。
Mockitoを使ったテストは以下のようになるでしょう。
(テスト対象)
public class Foo { public void doSomething() { final StringBuilder sb = new StringBuilder(); // sbに色々appendする処理 this.doOtherthing(sb.toString()); } void doOtherthing(String bar) { // 何かの処理 } }
(テスト)
import static org.mockito.Mockito.*; public class FooTest { @Test public void doSomething() { final Foo foo = spy(new Foo()); // Foo#doOtherthingの引数として渡されるはずの文字列 final String expectedDoOtherthingArgument = "foobarbaz"; // doOtherthingの中身を空実装にしておく // (ここはdoSomethingのテストなので、他の要素は排除すべき) doNothing().when(foo).doOtherthing(expectedDoOtherthingArgument); foo.doSomething(); // doOtherthingが期待通りに実行されているか検証 verify(foo).doOtherthing(expectedDoOtherthingArgument); } }
ま、Mockito#spyが大活躍!といった感じです。
ところが、
こんなクラスをテストする場合、上記の方法が使えません。
public class Bar { private String prop1; private int prop2; private List<String> prop3; // 以下、getter,setterのみ実装。 // equalsとhashCodeは実装されていない。 } public class Foo { public void doSomething() { final Bar bar = new Bar(); bar.setProp1("1"); bar.setProp2(2); bar.setProp3(Arrays.asList("3.1", "3.2")); this.doOtherthing(bar); } void doOtherthing(Bar bar) { // 何かの処理 } }
(テスト)
import static org.mockito.Mockito.*; public class FooTest { @Test public void doSomething() { final Foo foo = spy(new Foo()); // Foo#doOtherthingの引数として渡されるはずのBar final Bar expectedBar = new Bar(); expectedBar.setProp1("1"); expectedBar.setProp2(2); expectedBar.setProp3(Arrays.asList("3.1", "3.2")); doNothing().when(foo).doOtherthing(expectedBar); foo.doSomething(); // doOtherthingが期待通りに実行されているか検証…? verify(foo).doOtherthing(expectedBar); } }
このテストは失敗します。何故かというと、FooTestの
doNothing().when(foo).doOtherthing(expectedBar);
で渡しているexpectedBarにはequalsメソッドが実装されていないので、
たとえ事実上『等価』であるBarを渡しても、実際にFoo#doSomethingの中で作成されたBarと
等しいとはみなされないからです。
当然ながら、これもダメです。
(テスト)
import static org.mockito.Mockito.*; import static org.mockito.Matchers.*; public class FooTest { @Test public void doSomething() { final Foo foo = spy(new Foo()); // Foo#doOtherthingの引数として渡されるはずのBar final Bar expectedBar = new Bar(); expectedBar.setProp1("1"); expectedBar.setProp2(2); expectedBar.setProp3(Arrays.asList("3.1", "3.2")); // どんなBarであろうとも、とにかくdoOtherthingの中身を空実装にする doNothing().when(foo).doOtherthing(any(Bar.class)); foo.doSomething(); // doOtherthingが期待通りに実行されているか検証…? verify(foo).doOtherthing(expectedBar); } }
これだとdoNothingのところはうまく動きますが、結局のところ
verify(foo).doOtherthing(expectedBar);
での検証に失敗します。もちろん
verify(foo).doOtherthing(any(Bar.class));
とすれば動きますが、これでは単体テストの意味が無い。
この問題は、Barにequalsメソッドを実装すれば解決するわけですが、
それが常に出来るとは限らないわけです。(他社のコンポーネントだから手を入れられないとか)
ArgumentCaptorさんコンニチハ
そこでどうするかと、ArgumentCaptorさんの登場となるわけです。
こんな風に使います。
import static org.junit.Assert.*; import static org.mockito.Mockito.*; public class FooTest { @Test public void doSomething() { final Foo foo = spy(new Foo()); // 引数キャプター final ArgumentCaptor<Bar> barCaptor = ArgumentCaptor.forClass(Bar.class); // doOtherthingの中身を空実装にする。ついでに引数を捕獲する doNothing().when(foo).doOtherthing(barCaptor.capture()); foo.doSomething(); // 捕獲した引数 final Bar captoredBar = barCaptor.getValue(); assertEquals("1", captoredBar.getProp1()); assertEquals(2, captoredBar.getProp2()); assertEquals(Arrays.asList("3.1", "3.2"), captoredBar.getProp3()); } }
このように、引数を捕獲して後からassertできるというわけ。
ArgumentCaptorさんの弱点。それは…
特定のシチュエーションで確かに便利なArgumentCaptorさんですが、一点だけ注意が。
public class Foo { public void doSomething() { doOtherthing(..., "foobar"); } void doOtherthing(Bar bar, String s) { } } public class Bar { ... } public class FooTest { @Test public void doSomething() { final Foo foo = spy(new Foo()); // Foo#doOtherthingの引数として渡されるはずのBar final ArgumentCaptor<Bar> bar1Captor = ArgumentCaptor.forClass(Bar.class); // doOtherthingの中身を空実装にする。ついでに第一引数を捕獲する。 // 第二引数は"foobar"だと分かっているので直接指定…? doNothing().when(foo).doOtherthing(barCaptor.capture(), "foobar"); foo.doSomething(); // 以下略 } }
こんなふうにテストコードを書いて実行すると、
doNothing().when(foo).doOtherthing(barCaptor.capture(), "foobar");
の箇所でMockitoのInvalidUseOfMatchersExceptionがスローされてしまいます。
この例外は名前が示す通り、Mockitoの使い方を間違えている場合にスローされるものです。
org.mockito.exceptions.misusing.InvalidUseOfMatchersException: Invalid use of argument matchers! 3 matchers expected, 2 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")); For more info see javadoc for Matchers class. at XXX.doSomething(FooTest.java:??) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at java.lang.reflect.Method.invoke(Method.java:597) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:76) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:193) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:52) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:42) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:184) at org.junit.runners.ParentRunner.run(ParentRunner.java:236) at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:46) at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)
何故かというと、ArgumentCaptorは一種のMatcherとして振舞うから。
こちらの記事で解説している通りですが、『Matcherを使うなら全ての引数で使わないとダメ』
というルールに違反しているからです。(※ただしMatchers#eqを使えば、事実上Matcherの部分適用ができる事に注意)
というわけで、こんな風に修正すれば意図した通りに動きます。
public class FooTest { @Test public void doSomething() { final Foo foo = spy(new Foo()); // Foo#doOtherthingの引数として渡されるはずのBar final ArgumentCaptor<Bar> bar1Captor = ArgumentCaptor.forClass(Bar.class); // doOtherthingの中身を空実装にする。ついでに第一引数を捕獲する。 // // 第二引数は"foobar"だと分かっているがanyStringにしないとダメ。 // どうしても検証したいなら第二引数でも引数キャプターを使えばOK doNothing().when(foo).doOtherthing(barCaptor.capture(), anyString()); foo.doSomething(); // 以下略 } }