先日、ユニットテストで使っているJMockitのバージョンを上げる作業をしました(1.13 → 1.46)。その際に分かった、新旧JMockitの変更点やバージョンアップ時修正方法をメモします。
主に以下の2種類の人に役立つことを期待します。
- 今まで古いJMockitを使っていたが、最新版を使う人(つまり私と私の同僚)
- これからJMockitをアップデートする人
JMockit とは
JMockit は Java用のモックライブラリです。ユニットテストの中で、既存のメソッドの動作を差し替える(モックする)ことができます。
JMockitの公式ページ → The JMockit testing toolkit
差し替えは、インスタンスイニシャライザと特殊なインスタンス変数 result
や times
を使って定義します。
new Expectations() {{
obj.method(arg1, arg2, arg3);
result = value;
times = 2;
}};
かなり強力な操作もサポートしており、以下のような差し替えもできます(詳しくは公式ページ参照)。
- staticメソッドの差し替え
- オブジェクトの特定のメソッドのみ差し替え(Partial Mocking)
-
new
が返すオブジェクトの差し替え
JMockit のバージョンアップの特徴
2・3か月ごとにマイナーバージョンが上がります。過去のリリースはこちらのページから確認できます:
履歴からわかるように、JMockitは古い機能はどんどん切り捨てるポリシーです。例えばDeencapsulation.setField
というメソッドは2ヶ月・2バージョンで非推奨指定→廃止されています。
Version 1.45 (Jan 27, 2019):
Removed the Deencapsulation.setField methods, which were deprecated in version 1.44.Version 1.44 (Nov 25, 2018):
Deprecated the Deencapsulation.setField methods, in favor of @Tested and @Injectable.
したがって、一気にバージョンを上げようとすると、大量の変更が必要になり、作業が辛くなります。
- JMockitのバージョンを上げる(常に最新版に追従する)
- JMockit以外のライブラリに移行する
の、どちらかをお勧めします。
JMockit より Mockito の方がオススメ!
JMockitのライバルにはMockito があります。Mockito の方がユーザー数が多く、記法も自然です(特にRubyのRSpecに慣れた身としては)。
また、Mockito単体では通常のメソッドの差し替えしかできませんが、Powermock と組み合わせれば、JMockitと同様の強力な「汚い」変更もできるようになります。
// mockito の記法のイメージ
when(obj.method(arg1, arg2, arg3)).thenReturn(value);
したがって、可能ならJMockitからMockitoに移行することをお勧めします。
なお、今回のプロダクトでは、JMockitを使ったテストが大量にあったためMockitoに移行するのは諦めました。
バージョンアップ作業の流れ
以下のような作業でバージョンアップできます。
- JMockitのバージョンを上げる
- バージョンアップで廃止された機能などを書き換える
- コンパイルする
- コンパイルが通るまでコードを修正する
- テスト実行
- テストが通るまでコードを修正する
ただし「最新版では避けるべき書き方」で説明するように、コンパイルは通るが、意図した通りに動作しなくなるケースがあります。テストが失敗する場合は、そこを疑ってください。
なお、古いバージョンから一足飛びに最新版に上げるのは、変更点が多くなりすぎ破綻するので、オススメしません。
また、
- バージョン 1.x で廃止された機能が 1.y で復活しているので、1.xを飛ばして 1.yに上げるべき
- バージョン 1.(x+1)には後方互換のための機能があるので、1.xを飛ばして1.(x+1)に上げるべき
といったショートカットパスも無いと思います。地道に1バージョンずつ上げていくことをオススメします。
最新版のチュートリアルを読む
バージョンアップしたりテストを書いたりする前に最新版のチュートリアルを読みましょう。
JMockitはそもそもクセが強いライブラリです。「書き方が違うだけでMockitoと同じだろ」と思うと痛い目に会います。例えば @Mocked
であるクラスのモックオブジェクトを取得すると、そのクラスの全インスタンスがモックになります。
また、上述のように機能が大胆に改廃されおり、書き方がだいぶ変わっています。JMockitの経験者もコードに触る前に目を通してください。
主な廃止機能と書き換え方法
最新版のチュートリアルに目を通しましたか?
OK!
過去のバージョンの変更点のうち、影響が大きそうなものを説明します(細かい機能は申し訳ないが、リリースノートなどで調べてください)。
テストの実行方法
従来は JUnitのテストクラスに @RunWith(JMockit.class)
アノテーションをつけるとJMockitが読み込まれていました。
現在は方法が変わり、ユニットテストの実行時に、VMのオプションとして以下のように javaagent として JMockit の jar ファイルを指定すると、JMockitが読み込まれます。
-javaagent:/path/to/jmockit-1.46.jar
IntelliJ なら以下の場所で設定します。
「ツールバーの実行ボタン横のモジュール名」 → Edit Configurations → Templates → JUnit → Configurations → VM options
@RunWith(JMockit.class)
アノテーション
上述のように廃止されたので単に削除してください。
1引数の returns
1引数の returns
は、代わりに result =
を使ってください。2引数以上の returns
は引き続き使えます。
なおreturns
を1個1個書き換えるのは大変ですが、それについては別記事に書いたので参考にしてください(→ 私は如何にして JMockit の returns(Object) を安全に置換したか?)。
// Old
new Expectations() {{
obj.method(arg1, arg2, arg3); returns(value);
}};
// New
new Expectations() {{
obj.method(arg1, arg2, arg3); result = value;
}};
NonStrictExpectations
NonStrictExpectations
は、Expectations
の「差し替えたメソッドが呼び出されなくてもエラーにならないバージョン」でした。
以下のように Expectations
に置き換えた上で minTimes = 0
で最小呼び出し回数を0回に設定してください。
new Expectations() {{
mock.get(0); result = "1"; minTimes = 0;
}};
なお、「メソッドを差し替えたのに、それが呼び出されていない」のは、
- 実はそのメソッドを差し替える必要が無かった
- 意図した動作をしていない
のどちらかの場合が多いので、テストケースを見直してみてください。
StrictExpectations
単に Expectations に書き換えてください。
Deencapsulation
Deencapsulation は private なフィールドを参照したり変更したりする機能でした。
1. フィールドを package-private
ないし public
に変更する
Deencapsulation
で変更していたフィールドのアクセス制限を緩め、テストからは.
でアクセスするようにします。フィールドには Guava の @VisibleForTesting
アノテーションを付けます。
本体側コードに変更を加えることになりますが、アクセス修飾子の変更がバグに繋がることは普通は無いはずです1。
// Old
// 本体コード
class Spam {
// ...
private Foo field;
// ...
}
// テストコード
x = Deencapsulation.getField(obj, "field");
// New
// Old
// 本体コード
class Spam {
// ...
@VisibleForTesting
Foo field;
// ...
}
// テストコード
x = obj.field;
2. @Tested
と @Injectable
を使う
JMockitにはテスト用のDI機構があります。
Deencapsulation.setField
をテスト対象オブジェクトにモックをセットするために使っている場合は、DIに置き換えられます。
4 Instantiation and injection of tested classes
.newMockInstance()
モックインスタンスを @Mocked で取得するようにします。単純な書き換えはできません。
System
などのメソッドはモック
古いバージョンでは JMockit はどんなメソッドでも差し替えられましたが、
現在のバージョンでは System.currentTimeMillis
のようなネイティブメソッドは差し替えられません。
テストを再設計し、差し替え自体を不要にするか、ネイティブではないメソッドを差し替える形にしましょう。
今回バージョンアップしたプロダクトの、System.currentTimeMillis
を使っている部分は、代わりに new Date
を差し替える形に変えました。
最新版では避けるべき書き方
ドキュメントには明示されていませんが、旧バージョンでは問題なく動いていたし、コンパイルも通るのに、新バージョンでは動かなくなるパターンがあります。
引数・戻り値に別のメソッド呼び出しを書く
Expectations
の中で、メソッドの引数や戻り値として別のメソッド呼び出しを書くと、うまく差し替わらない場合があります。引数・戻り値はExpectations
の外で、一旦一時変数に代入してください。可読性の観点からも分けた方がよいでしょう。
// NG
new Expectations() {{
foo.method(getX(), getY()); result = getHogeList();
}};
// OK
X x = getX();
Y y = getY();
HogeList hogeList = getHogeList();
new Expectations() {{
foo.method(x, y); result = hogeList;
}};
引数ありExpectations
の中に無関係なメソッドを書く
クラスの特定のメソッドだけを差し替えたい時や、特定のインスタンスのメソッドだけを差し替えたい時には、Expectations
に引数にクラスやインスタンスを指定します。
このとき、Expectations
の引数と無関係なメソッド呼び出しを混ぜてしまうと、うまくモックできないことがあります。無関係なメソッド呼び出しは、別のExpectations
に分けましょう。可読性の観点からも分けた方がよいでしょう。
// NG
new Expectations(foo) {{
foo.method(x, y); result = v;
other.othersMethod(); result = value; // このメソッドが刺し変わらない
}};
// OK
new Expectations(foo) {{
foo.method(x, y); result = v;
}};
new Expectations() {{ // Expectations を分ける
other.othersMethod(); result = value;
}};
-
もちろん、テスト用に public にしているフィールド(本来はprivateにしたい)に本番コードでアクセスされると困りますが、
@VisibleForTesting
を使えばアクセスしてよいフィールドかどうかは区別できるはずです。 ↩