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回呼び出した場合
テストコードはこんな感じで。
@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));
}
};
}
お~~~成功。
複数回呼び出した場合
こんどはテストコードをこんな感じで。
@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));
}
};
}
おぉぉ~~~~~~これも成功。
パラメータを複数増やしてみると失敗した
第二引数を可変長パラメータにしているので、設定値を増やしてみる。
単一実行の場合
@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));
}
};
}
これも成功。
わかってきた。気がする。
複数回実行
@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.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で実施する単体テストの仕切りとしては問題ない。と自分に言い聞かせる。
ということで、値の退避用・検証用にこんなオブジェクトを作成。
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;
}
}
そしてテストコードはこんな感じに
/**
* 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));
}
これで目的の検証は成功した。