Posted at

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

More than 3 years have passed since last update.


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

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で実施する単体テストの仕切りとしては問題ない。と自分に言い聞かせる。

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


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


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