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

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

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

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

# JMockit とは

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

JMockitの公式ページ → [The JMockit testing toolkit](http://jmockit.github.io/index.html)

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

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

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

- staticメソッドの差し替え
- オブジェクトの特定のメソッドのみ差し替え（Partial Mocking）
- `new` が返すオブジェクトの差し替え

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

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

[JMockit - Development history](http://jmockit.github.io/changes.html)

履歴からわかるように、JMockitは**古い機能はどんどん切り捨てる**ポリシーです。例えば`Deencapsulation.setField` というメソッドは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](https://site.mockito.org/) があります。Mockito の方がユーザー数が多く、記法も自然です（特にRubyのRSpecに慣れた身としては）。

また、Mockito単体では通常のメソッドの差し替えしかできませんが、[Powermock](https://github.com/powermock/powermock) と組み合わせれば、JMockitと同様の強力な「汚い」変更もできるようになります。

```java
// mockito の記法のイメージ
when(obj.method(arg1, arg2, arg3)).thenReturn(value);
```

したがって、可能なら**JMockitからMockitoに移行することをお勧めします**。

なお、今回のプロダクトでは、JMockitを使ったテストが大量にあったためMockitoに移行するのは諦めました。

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

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

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

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

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

また、

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

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

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

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

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

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

[JMockit - Tutorial](http://jmockit.github.io/tutorial.html)

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

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

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`を１個１個書き換えるのは大変ですが、それについては別記事に書いたので参考にしてください（→ [私は如何にして JMockit の returns(Object) を安全に置換したか？](https://qiita.com/tonluqclml/items/bce1d657b4450f6b713e)）。

```java
// 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回に設定してください。

```java
new Expectations() {{
    mock.get(0); result = "1"; minTimes = 0;
}};
```

なお、「メソッドを差し替えたのに、それが呼び出されていない」のは、

- 実はそのメソッドを差し替える必要が無かった
- 意図した動作をしていない

のどちらかの場合が多いので、テストケースを見直してみてください。

## StrictExpectations

単に Expectations に書き換えてください。

## Deencapsulation

Deencapsulation は private なフィールドを参照したり変更したりする機能でした。

### 1. フィールドを `package-private` ないし `public` に変更する

`Deencapsulation` で変更していたフィールドのアクセス制限を緩め、テストからは`.`でアクセスするようにします。フィールドには Guava の `@VisibleForTesting` アノテーションを付けます。

本体側コードに変更を加えることになりますが、アクセス修飾子の変更がバグに繋がることは普通は無いはずです[^1]。

[^1]: もちろん、テスト用に public にしているフィールド（本来はprivateにしたい）に本番コードでアクセスされると困りますが、`@VisibleForTesting` を使えばアクセスしてよいフィールドかどうかは区別できるはずです。

```java
// 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](http://jmockit.github.io/tutorial/Mocking.html#tested)

## `.newMockInstance()`

モックインスタンスを @Mocked で取得するようにします。単純な書き換えはできません。

## `System` などのメソッドはモック

古いバージョンでは JMockit はどんなメソッドでも差し替えられましたが、
現在のバージョンでは `System.currentTimeMillis` のようなネイティブメソッドは差し替えられません。

テストを再設計し、差し替え自体を不要にするか、ネイティブではないメソッドを差し替える形にしましょう。

今回バージョンアップしたプロダクトの、`System.currentTimeMillis` を使っている部分は、代わりに `new Date`を差し替える形に変えました。

# 最新版では避けるべき書き方

ドキュメントには明示されていませんが、**旧バージョンでは問題なく動いていたし、コンパイルも通るのに、新バージョンでは動かなくなる**パターンがあります。

## 引数・戻り値に別のメソッド呼び出しを書く

`Expectations`の中で、メソッドの引数や戻り値として別のメソッド呼び出しを書くと、うまく差し替わらない場合があります。引数・戻り値は`Expectations`の外で、一旦一時変数に代入してください。可読性の観点からも分けた方がよいでしょう。

```java
// 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`に分けましょう。可読性の観点からも分けた方がよいでしょう。

```java
// 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;
}};
```