jmockitのwithCapture(List)でバグを発見したときの話

# jmockitのwithCaptureについて
Javaで作成したソースコードのテストコードにjmockitを使っていた。
私は途中参加だったのでなぜjmockitなのかは不明。
ソースコードにlogj4を使ったログ出力を大量に追加することになったので、jmockitを使ってテストコードを書くことに。
出力されたログを目検で確認するのはツライので、junitの中で自動化したい。
そこでjmockitのwithCaptureというものを発見した。
どうやらMock化したものに対して実行された際に渡されたパラメータを確認できるものらしい。
しかもAPI的に複数回実行される場合でもList形式で実行順にパラメータが確認できると。

**API**
http://jmockit.org/api1x/mockit/Verifications.html

# 実際に試してみた
今回はログ出力を部品化して
 `HogeUtils.logInfoId(String id, String... params)`
というものの呼び出し時のパラメータを検証することにした。

## まずはシンプルに1回呼び出した場合
テストコードはこんな感じで。

```java:CaptureSampleTest.java
    @Test
    public void captureSample1() {
        // Mock定義
        new NonStrictExpectations(HogeUtils.class) {
            {
                HogeUtils.logInfoId(anyString, (String[])any);
            }
        };

        // テスト実行
        final String id = "X1234";
        final String message = "test1";
        HogeUtils.logInfoId(id, message);

        // 実行結果検証
        new Verifications() {
            {
                // capture用に格納する変数を定義
                String captureId;
                String[] captureParams;
                // captureの実行
                HogeUtils.logInfoId(captureId = withCapture(), captureParams = withCapture());
                // captureした結果の検証
                assertThat(captureId, is(id));
                assertThat(captureParams[0], is(message));
            }
        };
    }
```

お～～～成功。

## 複数回呼び出した場合
こんどはテストコードをこんな感じで。

```java:CaptureSampleTest.java
    @Test
    public void captureSample2() {
        // Mock定義
        new NonStrictExpectations(HogeUtils.class) {
            {
                HogeUtils.logInfoId(anyString, (String[])any);
            }
        };

        // テスト実行
        final String id1 = "X1234";
        final String message1 = "test1";
        HogeUtils.logInfoId(id1, message1);
        final String id2 = "X5678";
        final String message2 = "test2";
        HogeUtils.logInfoId(id2, message2);
        final String id3 = "9012";
        final String message3 = "test3";
        HogeUtils.logInfoId(id3, message3);

        // 実行結果検証
        new Verifications() {
            {
                // capture用に格納する変数を定義
                List<String> captureId = new ArrayList<>();
                List<String> captureParams = new ArrayList<>();
                // captureの実行
                HogeUtils.logInfoId(withCapture(captureId), withCapture(captureParams));
                // 実行回数の検証
                times = 3;
                // captureした結果の検証
                assertThat(captureId.get(0), is(id1));
                assertThat(captureParams.get(0), is(message1));
                assertThat(captureId.get(1), is(id2));
                assertThat(captureParams.get(1), is(message2));
                assertThat(captureId.get(2), is(id3));
                assertThat(captureParams.get(2), is(message3));
            }
        };
    }

```

おぉぉ～～～～～～これも成功。

# パラメータを複数増やしてみると失敗した
第二引数を可変長パラメータにしているので、設定値を増やしてみる。

## 単一実行の場合

```java:CaptureSampleTest.java
    @Test
    public void captureSample3() {
        // Mock定義
        new NonStrictExpectations(HogeUtils.class) {
            {
                HogeUtils.logInfoId(anyString, (String[])any);
            }
        };

        // テスト実行
        final String id = "X1234";
        final String[] message = {"test1", "test2", "test3"};
        HogeUtils.logInfoId(id, message);

        // 実行結果検証
        new Verifications() {
            {
                // capture用に格納する変数を定義
                String captureId;
                String[] captureParams;
                // captureの実行
                HogeUtils.logInfoId(captureId = withCapture(), captureParams = withCapture());
                // captureした結果の検証
                assertThat(captureId, is(id));
                assertThat(captureParams, is(message));
            }
        };
    }

```

これも成功。
わかってきた。気がする。

## 複数回実行

```java:CaptureSampleTest.java
    @Test
    public void captureSample4() {
        // Mock定義
        new NonStrictExpectations(HogeUtils.class) {
            {
                HogeUtils.logInfoId(anyString, (String[])any);
            }
        };

        // テスト実行
        final String id1 = "X1234";
        final String[] message1 = {"test1", "BBB", "CCC"};
        HogeUtils.logInfoId(id1, message1);
        final String id2 = "X5678";
        final String[] message2 = {"test2", "DDD", "EEE"};
        HogeUtils.logInfoId(id2, message2);
        final String id3 = "9012";
        final String[] message3 = {"test3", "FFF", "GGG"};
        HogeUtils.logInfoId(id3, message3);

        // 実行結果検証
        new Verifications() {
            {
                // capture用に格納する変数を定義
                List<String> captureId = new ArrayList<>();
                List<String[]> captureParams = new ArrayList<>();
                // captureの実行
                HogeUtils.logInfoId(withCapture(captureId), withCapture(captureParams));
                // 実行回数の検証
                times = 3;
                // captureした結果の検証
                assertThat(captureId.get(0), is(id1));
                assertThat(captureParams.get(0), is(message1));
                assertThat(captureId.get(1), is(id2));
                assertThat(captureParams.get(1), is(message2));
                assertThat(captureId.get(2), is(id3));
                assertThat(captureParams.get(2), is(message3));
            }
        };
    }

```

**な、なんと、、、失敗！？！？**

```java:exception
java.lang.ClassCastException: java.lang.String cannot be cast to [Ljava.lang.String;
	at com.example.CaptureSampleTest$8.<init>(CaptureSampleTest.java:187)
	at com.example.CaptureSampleTest.captureSample4(CaptureSampleTest.java:176)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.lang.reflect.Method.invoke(Method.java:606)
	at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:86)
	at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:675)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
	at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)
```

なぜだ・・・
StringをString配列に変換するのに失敗しているっぽい。
なぜ変換が必要なのだ？？
String配列をString配列としてcaptureしようとしているだけなのに。。。
わからん。
assertのところで型変換しようとして失敗しているらしい。
*デバッグしてみると、配列を期待したcaptureの結果は配列の先頭項目の"test1"だけだった。。。*

# jmockitのwithCapture(List)のバグだと判明
試しにインターフェースの可変長引数を普通の配列に変えてみた。
 `HogeUtils.logInfoId(String id, String[] params)`

**すると成功した・・・**

jmockitの中のソースみてもよくわからなかったので、GitHubのjmockitにissueを作ってみた。

https://github.com/jmockit/jmockit1/issues/271

その日の夜には開発者の方からコメントが。

*It seems to be a bug in the use of  withCapture(List)  for a varargs parameter.*

バグ！ orz

# 苦しい紛れの回避策
バグだと判明して心の中のモヤモヤはすっきりしたけど、これじゃあテストができないので自力で回避策を模索。

結果、Mock化した対象メソッド実行時にDelegateクラスを使って対象メソッドの挙動を書き換えて、中で渡されたパラメータを別の場所に退避して後から検証するようにした。
対象メソッドの中はlog4jのloggerを呼ぶだけだし、junitで実施する単体テストの仕切りとしては問題ない。と自分に言い聞かせる。

ということで、値の退避用・検証用にこんなオブジェクトを作成。

```java:LogBean.java
public class LogBean {

    private List<String> idList = new ArrayList<>();
    private List<String[]> msgList = new ArrayList<>();

    public List<String> getIdList() {
        return idList;
    }

    public void setIdList(List<String> idList) {
        this.idList = idList;
    }

    public List<String[]> getMsgList() {
        return msgList;
    }

    public void setMsgList(List<String[]> msgList) {
        this.msgList = msgList;
    }

}
```

そしてテストコードはこんな感じに

```java:CaptureSampleTest.java

    /**
     * INFOメッセージ確認用
     */
    LogBean infoLogBean;

    /**
     *  テストケース実行前に退避・検証用Beanを初期化し、試験対象をMock化する
     */
    @Before
    public void setUp(){
        infoLogBean = new LogBean();

        new NonStrictExpectations(HogeUtils.class) {
            {
                HogeUtils.logInfoId(anyString, (String[])any);
                result = new Delegate() {
                    void logInfoId(String msgId, String... any) {
                        infoLogBean.getIdList().add(msgId);
                        infoLogBean.getMsgList().add(any);
                    }
                };
            }
        };
    }

    @Test
    public void captureSample5() {
        // テスト実行
        final String id1 = "X1234";
        final String[] message1 = {"test1", "BBB", "CCC"};
        HogeUtils.logInfoId(id1, message1);
        final String id2 = "X5678";
        final String[] message2 = {"test2", "DDD", "EEE"};
        HogeUtils.logInfoId(id2, message2);
        final String id3 = "9012";
        final String[] message3 = {"test3", "FFF", "GGG"};
        HogeUtils.logInfoId(id3, message3);

        // captureした結果の検証
        assertThat(infoLogBean.getIdList().get(0), is(id1));
        assertThat(infoLogBean.getMsgList().get(0), is(message1));
        assertThat(infoLogBean.getIdList().get(1), is(id2));
        assertThat(infoLogBean.getMsgList().get(1), is(message2));
        assertThat(infoLogBean.getIdList().get(2), is(id3));
        assertThat(infoLogBean.getMsgList().get(2), is(message3));
    }

```

これで目的の検証は成功した。