4
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

JMockit と共に生きる者ためのメモ

Last updated at Posted at 2019-08-28

先日、ユニットテストで使っているJMockitのバージョンを上げる作業をしました(1.13 → 1.46)。その際に分かった、新旧JMockitの変更点やバージョンアップ時修正方法をメモします。

主に以下の2種類の人に役立つことを期待します。

  • 今まで古いJMockitを使っていたが、最新版を使う人(つまり私と私の同僚)
  • これからJMockitをアップデートする人

JMockit とは

JMockit は Java用のモックライブラリです。ユニットテストの中で、既存のメソッドの動作を差し替える(モックする)ことができます。

JMockitの公式ページ → The JMockit testing toolkit

差し替えは、インスタンスイニシャライザと特殊なインスタンス変数 resulttimes を使って定義します。

new Expectations() {{
  obj.method(arg1, arg2, arg3);
  result = value;
  times = 2;
}};

かなり強力な操作もサポートしており、以下のような差し替えもできます(詳しくは公式ページ参照)。

  • staticメソッドの差し替え
  • オブジェクトの特定のメソッドのみ差し替え(Partial Mocking)
  • new が返すオブジェクトの差し替え

JMockit のバージョンアップの特徴

2・3か月ごとにマイナーバージョンが上がります。過去のリリースはこちらのページから確認できます:

JMockit - Development history

履歴からわかるように、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に移行するのは諦めました。

バージョンアップ作業の流れ

以下のような作業でバージョンアップできます。

  1. JMockitのバージョンを上げる
  2. バージョンアップで廃止された機能などを書き換える
  3. コンパイルする
  4. コンパイルが通るまでコードを修正する
  5. テスト実行
  6. テストが通るまでコードを修正する

ただし「最新版では避けるべき書き方」で説明するように、コンパイルは通るが、意図した通りに動作しなくなるケースがあります。テストが失敗する場合は、そこを疑ってください。

なお、古いバージョンから一足飛びに最新版に上げるのは、変更点が多くなりすぎ破綻するので、オススメしません。

また、

  • バージョン 1.x で廃止された機能が 1.y で復活しているので、1.xを飛ばして 1.yに上げるべき
  • バージョン 1.(x+1)には後方互換のための機能があるので、1.xを飛ばして1.(x+1)に上げるべき

といったショートカットパスも無いと思います。地道に1バージョンずつ上げていくことをオススメします。

最新版のチュートリアルを読む

バージョンアップしたりテストを書いたりする前に最新版のチュートリアルを読みましょう。

JMockitはそもそもクセが強いライブラリです。「書き方が違うだけでMockitoと同じだろ」と思うと痛い目に会います。例えば @Mocked であるクラスのモックオブジェクトを取得すると、そのクラスの全インスタンスがモックになります

また、上述のように機能が大胆に改廃されおり、書き方がだいぶ変わっています。JMockitの経験者もコードに触る前に目を通してください。

JMockit - Tutorial

主な廃止機能と書き換え方法

最新版のチュートリアルに目を通しましたか?

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;
}};
  1. もちろん、テスト用に public にしているフィールド(本来はprivateにしたい)に本番コードでアクセスされると困りますが、@VisibleForTesting を使えばアクセスしてよいフィールドかどうかは区別できるはずです。

4
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?