例えば次のようなクラスFooをテストしたいとします。
(かなり恣意的なサンプルですが勘弁して下さい)
import java.sql.*; public class Foo { public void doSomething(String url) { final Connection conn1 = this.getConnection(url); final Connection conn2 = this.getConnection(url); doOtherthing(conn1, conn2); } void doOtherthing(Connection conn1, Connection conn2) { // 2つのコネクションを使う必要がある何か } Connection getConnection(String url) { // (略) } }
で、テストコードを次のように書いてみました。
public class FooTest { @Test public void testDoSomething() { // みんな大好きspyさん final Foo foo = spy(new Foo()); final String url = "..."; final Connection conn1 = mock(Connection.class); final Connection conn2 = mock(Connection.class); doReturn(conn1).when(foo).getConnection(url); doReturn(conn2).when(foo).getConnection(url); doNothing().when(foo).doOtherthing(conn1, conn2); foo.doSomething(url); // 第一引数がconn1, 第二引数がconn2のハズ verify(foo).doOtherthing(conn1, conn2); } }
しかし、このテストは途中のdoNothingで失敗します。
なぜなら、『モックに対して同じメソッドに同じ引数を与えたとき、最後に指定した振る舞いが適用される』
というルールがあるからです。
つまり、テストを実行すると実際は
doOtherthing(conn2, conn2); // conn2を2回使っている
となってしまっているワケです。
thenReturnを使えば解決?
『同じ引数に対して、2通りの戻り値を与える』
には、thenReturnを使えばいいはず。だから次のようにしてみました。
public class FooTest { @Test public void testDoSomething() { final Foo foo = spy(new Foo()); final String url = "..."; final Connection conn1 = mock(Connection.class); final Connection conn2 = mock(Connection.class); // thenReturnに複数の戻り値を指定すれば、先頭から順にreturnしてくれる when(foo.getConnection(url)).thenReturn(conn1, conn2); doNothing().when(service).doOtherthing(conn1, conn2); foo.doSomething(url); // 第一引数がconn1, 第二引数がconn2のハズ verify(foo).doOtherthing(conn1, conn2); } }
で、実行すると。。。今度は何とgetConnectionの中身が実行されてしまいました。
単体テストで実際にJDBCコネクションを張るわけがないので、当然エラーになって終了。
さて、その理由ですが、
when(foo.getConnection(url)).thenReturn(conn1, conn2);
の中身である
foo.getConnection(url)
に注目してください。ここで、fooはspyにより一部モック化されたインスタンスです。
で、この時点ではまだ、getConnectionへモックの振る舞いを指定していない。
ということは、thenReturnが呼ばれる前に、本物のgetConnectionの中身が先に実行されてしまうというワケ。
doReturnがthenReturnと同等の機能(可変長引数)を備えていれば、
doReturn(conn1, conn2).when(foo).getConnection(url);
と書けるので、きっと問題なく(doReturnの戻り値はモック化されたインスタンス)動作すると思うのですが、
残念なことに、doReturnは可変長引数でない。。。ムググ。
こんな時にはAnswer
少々前置きが長かったですが、こんな時にはAnswerを使えばいいわけです。こんな感じ。
public class FooTest { @Test public void testDoSomething() { final Foo foo = spy(new Foo()); final String url = "..."; final Connection conn1 = mock(Connection.class); final Connection conn2 = mock(Connection.class); final Answer<Connection> answerOfGetConnection = new Answer<Connection>() { private int counter; @Override public Connection answer(InvocationOnMock invocation) { if ((counter == 0) && invocatino.getArguments[0].equals(url)) { counter++; return conn1; } else if ((counter == 1) && invocatino.getArguments[0].equals(url)) { return conn2; } else { throw new org.junit.AssertionFailedError("なんかヘン"); } } }; doAnswer(answerOfGetConnection).when(service).getConnection(url); doNothing().when(foo).doOtherthing(conn1, conn2); foo.doSomething(url); verify(foo).doOtherthing(conn1, conn2); } }
Answerは単純にコールバックなので、何でもできます。
コードが冗長になっちゃうのが悩みどころですが、まぁしゃあない。
というかぶっちゃけ、
public class FooTest { @Test public void testDoSomething() { final String url = "..."; final Connection conn1 = mock(Connection.class); final Connection conn2 = mock(Connection.class); // 匿名クラスにもspyは適用可能。さすがspyさん。。。!! final Foo foo = spy(new Foo() { private int counter; @Override public Connection getConnection(String url) { if (counter == 0) { counter++; return conn1; } else if (counter == 1) { return conn2; } else { throw new org.junit.AssertionFailedError("なんかヘン"); } } }); doNothing().when(foo).doOtherthing(conn1, conn2); foo.doSomething(url); verify(foo).doOtherthing(conn1, conn2); } }
ってやった方がまだマシじゃね?という話もあるのですが、
まあそこは言わない約束ということで。