TL;DR
-
Mockito.mock(Class, Answer)でデフォルトの返り値を設定しよう。
Fluent Interface についてざっくり復習
最近は Fluent なインタフェースを取り入れたものが多くなってきています。
例えば、以下のようなインタフェースは Fluent なインタフェースと呼ばれます。
public interface SomethingFluent {
SomethingFluent foo();
SomethingFluent bar();
SomethingFluent baz();
}
これを使うコードを見てみると、メソッドの呼び出しを数珠つなぎにすることで、オブジェクトにどのような状態の変化が起こるかがひと目で理解できるようになっていることがわかると思います。
SomethingFluent o1 = new ConcreteSomethingFluent().foo().bar(); // foo と bar を呼んだ状態の SomethingFluent
SomethingFluent o2 = new ConcreteSomethingFluent().baz().bar(); // baz と bar を呼んだ状態の SomethingFluent
SomethingFluent o3 = new ConcreteSomethingFluent().foo().bar().baz(); // 全てを呼んだ状態の SomethingFluent
Fluent Interface をモックする
ここで、このSomethingFluentに依存したクラスTestTargetのテストを考えます。
public class TestTarget {
SomethingFluent fluent;
public TestTarget(SomethingFluent fluent) {
this.fluent = fluent;
}
public method1() {
fluent.foo();
}
public method2() {
fluent.foo().bar();
}
}
コンストラクタ経由でSomethingFluentの依存を注入できるので、ここでSomethingFluentのモックを流し込みます。
Mockito を使えば簡単ですね。もし Mockito を使わないのであれば、自分でモック実装を作ることになります。
public class TestTargetTest {
private SomethingFluent dependency;
private TestTarget target;
@Before
public void setUp() {
dependency = mock(SomethingFluent.class);
target = new TestTarget(dependency);
}
@Test
public void test_method1() {
target.method1();
verify(dependency).foo();
verify(dependency, never()).bar();
}
@Test
public void test_method2() {
target.method2(); // causing NullPointerException
verify(dependency).foo();
verify(dependency).bar();
}
}
Mockito.mock(Class)を使う場合、モックオブジェクトが持つ各メソッドの返り値は、voidでない限りnullか各型のデフォルト値になります。つまり、SomethingFluentの場合は、全てのメソッドがSomethingFluent型のオブジェクトを返しますので、モックしたメソッドを呼び出すと常にnullが返ってくることになります。
すると、TestTarget#method1()は困ったことは起こりませんが、TestTarget#method2()では困ったことが起こります。
なぜなら、メソッド呼び出しを数珠つなぎにしてしまっているので、SomethingFluent#foo()を呼んだ返り値がnullとなり、bar()の呼び出しでNullPointerExceptionとなるからです。
デフォルトの返り値を設定する
愚直には、Mockito で各メソッドの返り値をそれぞれごとに設定できるようになっているため、それを全てのメソッドに対して行うことになりますが、いかんせんそれは非常に面倒です。そして大抵そのことが忘れ去られ、新しいメソッドを追加したときに無駄にテストがこけてしまいます。
そこで登場するのが、Mockito.mock(Class, Answer)です。
第二引数のAnswerは、各モックメソッドがデフォルトで返す返り値を設定するものです。このAnswerを適切に設定することで、null以外の、任意のオブジェクトが個別に設定しなくても返ってくるようになります。
今回は、Fluent なインタフェースですから、自分自身を返す必要があります。そこで、以下のようにモックメソッドの呼び出しによってモックオブジェクトを返すようにします。
public class TestTargetTest {
private SomethingFluent dependency;
private TestTarget target;
@Before
public void setUp() {
dependency = mock(SomethingFluent.class, (Answer) invocation -> dependency);
target = new TestTarget(dependency);
}
@Test
public void test_method1() {
target.method1();
verify(dependency).foo();
verify(dependency, never()).bar();
}
@Test
public void test_method2() {
target.method2(); // no problem!
verify(dependency).foo();
verify(dependency).bar();
}
}
これで、Fluent なインタフェースの返り値が無事設定できました。