Java
jMockit

JMockit1.4→1.30へのアップデート時にコケたことまとめ

More than 1 year has passed since last update.

Java8u74→8u102ぐらいに上げた際に、どうやらJMockitも上げなきゃならんってことになりましたが、

使ってるJMockitが古すぎて、コンパイルエラーになるやら、実行時エラーになるやらわりとてんやわんやだったので、

備忘録としてまとめることにしました。

※随時更新中


環境

Java8u102(8u74でもたぶん動くかと)

maven3系


change log

だいたい書いてあるけど、実際にどう書けばいいのかわからないのもありつつ

JMockit Development history


主に参考にさせていただいたサイト



  • JMockit使い方メモ
    だいたい載ってて、しかも、詳しく書き方も載っててマジ感謝です。


まずはライブラリのバージョンをアップ


JMockit 1.4


pom.xml

        <dependency>

<groupId>com.googlecode.jmockit</groupId>
<artifactId>jmockit</artifactId>
<version>1.4</version>
<scope>test</scope>
</dependency>


JMockit 1.30

groupすら変わってることにビビります。(よくあるけどさ・・・)

※追記:ご指摘いただきました。昔から2つグループがあったらしいです。が、統合されたんですかね?


pom.xml

        <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に書き換えます。


修正前.java

@Test

public void test(@Mocked(methods="readLine") final BufferedReader br) {
new NonStrictExpectations() {
{
br.readLine();
result = new IOException();
}
};
}


修正後.java

@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.*;



修正前.java

setField(hoge, "aaaa", "aaaa");

invoke((hoge, "privateMethod");


変更後.java

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でチェックですれば良さそう。


HogeTest.java

        new Expectations() {{

hoge.method(); minTimes=0;
hoge.method2(); maxTimes=0;
}};


まあ、これをつけると、呼ばれたか呼ばれてないかわからなくなるので、その辺はVerificationsでチェックするようにしたらいいんじゃないかなと。

※追記:maxTimes=0;でチェックできるじゃんと気づきました。


EnumのMockの書き方が変わった?

こんな書き方してるやつが、NullPointerExceptionで落ちるようになった。


修正前.java

@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?


修正後.java

@Test

public void test() { // MockUpで書くので、引数は消す
new MockUp<HogeEnum>() {
@Mock
public String getHoge() {
return "/NOTHING/DATA.mf";
}
};
}


NonStrictExpectations→Expectationsに変えたことによるMockインスタンスの挙動変更?

こんな書き方してたやつをNonStrictExpectations→Expectationsに変えたら、failになってしまった。


変更前.java

    @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を明示的に指定する


修正案.java

    @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 初期化ブロックをモック化するを参考に使って書き直してみました。


Hoge.java

// Applicationは自作クラスです

private final static Application application = Application.getInstance();


修正前.java

Hoge hoge = new Hoge();

Deencapsulation.setField(Hoge.class , application);


修正後.java

        new MockUp<Hoge>() {

@Mock
void $clinit() {
Deencapsulation.setField(Hoge.class ,hoge);
}
};

staticブロックで宣言してなくても$clinit()で代入出来るのか。謎。


Deencapsulation.invokeも挙動が変わったみたい

invokeも一部駄目でした。動くやつと動かない差がわからない。。

※Expectations内でinvokeしてるやつは、MockUpで書かないと駄目っぽいです。ただいま確認中

これもMockUp使うと動きました。MockUp強すぎ。


PrivateHoge.java

// こんなメソッドがあったとする

private Hoge getHoge() {
return hoge;
}


修正前.java

new Expectations() {{

Deencapsulation.invoke(privateHoge, "getHoge");
}};

MockUp使って書き直し


修正後.java

        new Expectations() {{

new MockUp<PrivateHoge>() {
@Mock
Hoge getHoge() {
return hogehoge;
}
};
}};

MockUp内のMock化したメソッドなら、Deencapsulation.invokeも使えるみたいです。

NullPointerって言ってるから、明示的にしてからinvokeしろってことなのかなぁ


修正案その2.java

        new Expectations() {{

new MockUp<PrivateHoge>() {
@Mock
Hoge getHoge() {
Deencapsulation.invoke(privateHoge, "getHoge");
}
};
}};


動的部分モック化の書き方が変わった模様

Expectations内で@Mockedインスタンス宣言してたやつらがnullを返すようになりました。

ちなみに部分モックのことをPartial Mockingと言うらしいです。へぇー

jMockit Verifications 部分モッキングとか


動かなくなったやつ.java

        new Expectations(patternManager) {

@Mocked Hoge hoge;
{
System.out.println(hoge);
System.out.println(hoge.hoge);



修正案


  1. 単体テストのインスタンスフィールドに変える

  2. 単体テストの実行メソッドの引数で@Mocked使って宣言する→
    テストメソッドの引数で定義する

のどちらかで。


コンストラクタの戻り値は返せなくなった

コンストラクタの戻り値は指定できない

これが結構難儀で格闘中


代替案①

Accessing the invocation context にあるように$initメソッドでコンストラクタと同じ処理が出来るっぽい。シグニチャ合わせればよい。


HogeTest.java

new MockUp<File>() {

@Mock
void $init(File file, String path)
{
// new File(file, path) のコンストラクタをモック化出来る
}

ちなみに、Invocationを先頭の引数に加えると、モックインスタンスも取れるっぽい。


SystemクラスとかネイティブクラスもMockUpで書けばいいらしい


その他、参考にさせていただいたサイト様