LoginSignup
6
9

More than 5 years have passed since last update.

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

Posted at

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回呼び出した場合

テストコードはこんな感じで。

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));
            }
        };
    }

お~~~成功。

複数回呼び出した場合

こんどはテストコードをこんな感じで。

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));
            }
        };
    }

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

パラメータを複数増やしてみると失敗した

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

単一実行の場合

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));
            }
        };
    }

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

複数回実行

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));
            }
        };
    }

な、なんと、、、失敗!?!?

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を作ってみた。

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

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

バグ! orz

苦しい紛れの回避策

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

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

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

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;
    }

}

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

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));
    }

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

6
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
9