(注:このブログはもう更新していません)この日記は私的なものであり所属会社の見解とは無関係です。 GitHub: takahashikzn

[クラウド帳票エンジンDocurain]

doAnswerの使いどころ

例えば次のようなクラス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);
    }
}

ってやった方がまだマシじゃね?という話もあるのですが、
まあそこは言わない約束ということで。