Java8u74→8u102ぐらいに上げた際に、どうやらJMockitも上げなきゃならんってことになりましたが、
使ってるJMockitが古すぎて、コンパイルエラーになるやら、実行時エラーになるやらわりとてんやわんやだったので、
備忘録としてまとめることにしました。
※随時更新中
環境
Java8u102(8u74でもたぶん動くかと)
maven3系
change log
だいたい書いてあるけど、実際にどう書けばいいのかわからないのもありつつ
JMockit Development history
主に参考にさせていただいたサイト
- JMockit使い方メモ だいたい載ってて、しかも、詳しく書き方も載っててマジ感謝です。
まずはライブラリのバージョンをアップ
JMockit 1.4
<dependency>
<groupId>com.googlecode.jmockit</groupId>
<artifactId>jmockit</artifactId>
<version>1.4</version>
<scope>test</scope>
</dependency>
JMockit 1.30
groupすら変わってることにビビります。(よくあるけどさ・・・)
※追記:ご指摘いただきました。昔から2つグループがあったらしいです。が、統合されたんですかね?
<dependency>
<groupId>org.jmockit</groupId>
<artifactId>jmockit</artifactId>
<version>1.30</version>
<scope>test</scope>
</dependency>
1. コンパイルエラーを解消する
NonStrictExpectationsをExpectationsに変更する
Version 1.23でDeprecated、Version 1.25で削除になってます。
Version 1.23 (Apr 24, 2016):
Deprecated the NonStrictExpectations class. Existing tests should use Expectations instead.Version 1.25 (Jun 26, 2016):
Removed the NonStrictExpectations class, which was deprecated in version 1.23.
とりあえず、コンパイルエラーをなんとかしたいので、盲目的に変えていきます。
NonStrictExpectationsの使い方は以下のリンクでまとめてくださってます(もう使えないけど。。)
jMockitがいいよ Verification編
NonStrictExpectationsとExpectationsとの違いは
- Expectations 実行順序、実行回数などすべて記録。かなり厳密。
- NonStrictExpectations ちょっとゆるめ。実行されてもされなくてもスルーされる。
みたいな違いがあるらしいです。詳しくは以下のリンクでわかりやすくまとめてくださってます。感謝。
JMockit使い方メモ NonStrictExpectations
テスト実行時に落ちるので、これは後ほど修正します。
@Mocked(methods="readLine")とかアノテーション内の引数を指定してるやつ
Version 1.5でDeprecated 、Version 1.6で『methods』『inverse』属性は削除されてます。
Version 1.5 (Oct 20, 2013):
Deprecated the "methods" and "inverse" attributes of @Mocked. Existing uses like @Mocked(methods = {"method1", "method2"}) should simply be rewritten as @Mocked({"method1", "method2"}) (using the default value attribute). As for the inverse attribute, specify what is to be mocked instead.
Version 1.6 (Dec 23, 2013):
Removed the "methods" and "inverse" attributes of the @Mocked annotation, which were deprecated in release 1.5. Tests still using "@Mocked(methods = {...}) should instead use the default "value" attribute.
今度から@Mocked(methods = {...})使ってね、と言ってるわりにはこの方法もVersion 1.19で削除されちゃいます。悲しい。
Version 1.19 (Aug 23, 2015):
Removed the "value" attribute of @Mocked, which was deprecated in release 1.17. Existing tests using "@Mocked({"someMethod", "anotherMethod", ...})" should simply omit the attribute, or use partial mocking as applied with new Expectations(objToPartiallyMock) { ... }, or apply a MockUp class.
Expectationsの引数で書く方法は、その後もいろいろゴニョゴニョあって、使えるんだか使えないんだかわからないので、
おとなしくMockUpに書き換えます。
@Test
public void test(@Mocked(methods="readLine") final BufferedReader br) {
new NonStrictExpectations() {
{
br.readLine();
result = new IOException();
}
};
}
@Test
public void test() { // MockUp<T>で定義するので、引数は削除する。あるとエラーになる
new MockUp<BufferedReader>() {
@Mock
public String readLine() throws IOException{
throw new IOException();
}
};
}
ちなみに、BufferedReaderのこの場合だと、@Mockedを@Injectableに変えても良さそう(というか、どちらも同じ結果になった)
どっちがいいのかなぁ。
詳しくはこちら→JMockit はじめの一歩 @Injectable
setField, invokeはDeencapsulationのstaticメソッドに変更する
setField, invokeとかprivateなやつらに対して操作を行うのは、Invocationsクラスだったようなんですが、Version 1.6にて削除されたようです。
Version 1.6 (Dec 23, 2013):
Removed all Reflection-based utility methods from the Invocations class (the base for Expectations and Verifications), in favor of using the static methods of identical signatures in the Deencapsulation class. This change in the API will not require any changes to test code, except that test classes using these methods should now include the following "static import": import static mockit.Deencapsulation.*;
setField(hoge, "aaaa", "aaaa");
invoke((hoge, "privateMethod");
Deencapsulation.setField(hoge, "aaaa", "aaaa");
Deencapsulation.invoke((hoge, "privateMethod");
ちなみに、private static finalのフィールドにセットしようとすると、Can not set static finalって怒られる場合があるので、その場合は後述Deencapsulation.setFieldの挙動が変わったらしいを参考に。
2. テストでエラーになるものを修正する
Missing 1 invocation to: ~ Caused by: Missing invocations
これでコケるやつはNonStrictExpectationsをExpectationsに変更するの影響です。
このメソッド呼ばれてねーよ!って怒っております。サーセン。
メソッドの記録が厳密になった故のエラーですね。
回避策としては、メソッドの最小期待実行回数を0にしちゃいます。
ちなみに、呼ばれていないことを明示的に示したい場合は、maxTimes=0でチェックですれば良さそう。
new Expectations() {{
hoge.method(); minTimes=0;
hoge.method2(); maxTimes=0;
}};
まあ、これをつけると、呼ばれたか呼ばれてないかわからなくなるので、その辺はVerificationsでチェックするようにしたらいいんじゃないかなと。
※追記:maxTimes=0;でチェックできるじゃんと気づきました。
EnumのMockの書き方が変わった?
こんな書き方してるやつが、NullPointerExceptionで落ちるようになった。
@Test
public void test(@Mocked final HogeEnum hogeEnum) {
new Expectations() {
@Mocked
HogeEnum hogeEnum;
{
hogeEnum.getHoge();
result = "hogehoge";
}
};
どうやらMockUpに書き換えれば良いらしい。
How to mock this in an Enum class using jmockt?
@Test
public void test() { // MockUpで書くので、引数は消す
new MockUp<HogeEnum>() {
@Mock
public String getHoge() {
return "/NOTHING/DATA.mf";
}
};
}
NonStrictExpectations→Expectationsに変えたことによるMockインスタンスの挙動変更?
こんな書き方してたやつをNonStrictExpectations→Expectationsに変えたら、failになってしまった。
@Test
public void test() throws Exception {
final Hoge hoge1 = new Hoge("hogehoge");
new NonStrictExpectations() {
{
test.getHoge("hogehoge");
result = hoge1;
}
};
assertThat(test.getHoge("hogehoge"), hoge1);
assertThat(test.getHoge("fugafuga"), null); // ここでnullにはならない!
}
デバックしてみたら、Hogeのインスタンス自体は作られてしまってた。(valueとかはnullなんだけど)
ここ見ると、JMockit使い方メモ モック化されたオブジェクトのメソッドが返す値 とあるので、挙動通りではあるのですが、引数によって返すオブジェクト変えたいっていうか、ある引数以外のときはnull返したいんですよ。
※追記:nullを返したい場合は明示的に指定しないとだめらしい。ここに書いてました。→ JMockit使い方メモ モックが返す値を null にしたい場合
※2017/01/20追記:7tsunoさんよりコメントいただきました。きっちりやるならDelegate使うのが良いですね。詳しくは→Delegateでのマッチング
修正方法
引数違う場合のresultを明示的に指定する
@Test
public void test() throws Exception {
final Hoge hoge1 = new Hoge("hogehoge");
new Expectations() {
{
test.getHoge("hogehoge");
result = hoge1;
test.getHoge("fugafuga");
result = null;
}
};
assertThat(test.getHoge("hogehoge"), hoge1);
assertThat(test.getHoge("fugafuga"), null);
}
Deencapsulation.setFieldの挙動が変わったらしい
private static final なフィールドの書き換えが1.4では動いてたんですが、どうやら挙動が変わったらしく、java.lang.RuntimeException: java.lang.IllegalAccessException: Can not set static final ~ が発生するようになりました。(セットする対象はプリミティブではないです。セットしたのはMockオブジェクト。)
Deencapsulation.setField() is broken for JDK8 after I upgrade to v1.24
Ok, I updated our tests to use a MockUp instead. This is cleaner anyway.
え、MockUp 使えってことですか?
というわけで、static 初期化ブロックをモック化するを参考に使って書き直してみました。
// Applicationは自作クラスです
private final static Application application = Application.getInstance();
Hoge hoge = new Hoge();
Deencapsulation.setField(Hoge.class , application);
new MockUp<Hoge>() {
@Mock
void $clinit() {
Deencapsulation.setField(Hoge.class ,hoge);
}
};
staticブロックで宣言してなくても$clinit()で代入出来るのか。謎。
Deencapsulation.invokeも挙動が変わったみたい
invokeも一部駄目でした。動くやつと動かない差がわからない。。
※Expectations内でinvokeしてるやつは、MockUpで書かないと駄目っぽいです。ただいま確認中
これもMockUp使うと動きました。MockUp強すぎ。
// こんなメソッドがあったとする
private Hoge getHoge() {
return hoge;
}
new Expectations() {{
Deencapsulation.invoke(privateHoge, "getHoge");
}};
MockUp使って書き直し
new Expectations() {{
new MockUp<PrivateHoge>() {
@Mock
Hoge getHoge() {
return hogehoge;
}
};
}};
MockUp内のMock化したメソッドなら、Deencapsulation.invokeも使えるみたいです。
NullPointerって言ってるから、明示的にしてからinvokeしろってことなのかなぁ
new Expectations() {{
new MockUp<PrivateHoge>() {
@Mock
Hoge getHoge() {
Deencapsulation.invoke(privateHoge, "getHoge");
}
};
}};
動的部分モック化の書き方が変わった模様
Expectations内で@Mockedインスタンス宣言してたやつらがnullを返すようになりました。
ちなみに部分モックのことをPartial Mockingと言うらしいです。へぇー
jMockit Verifications 部分モッキングとか
new Expectations(patternManager) {
@Mocked Hoge hoge;
{
System.out.println(hoge);
System.out.println(hoge.hoge);
修正案
- 単体テストのインスタンスフィールドに変える
- 単体テストの実行メソッドの引数で@Mocked使って宣言する→ テストメソッドの引数で定義する
のどちらかで。
コンストラクタの戻り値は返せなくなった
これが結構難儀で格闘中
代替案①
Accessing the invocation context にあるように$initメソッドでコンストラクタと同じ処理が出来るっぽい。シグニチャ合わせればよい。
new MockUp<File>() {
@Mock
void $init(File file, String path)
{
// new File(file, path) のコンストラクタをモック化出来る
}
ちなみに、Invocationを先頭の引数に加えると、モックインスタンスも取れるっぽい。