おおくまねこ

職業プログラマーです。興味のある話題を書いています。

mockito でコンストラクターの mock を使ったテストをしたい

はじめに

今回は「mockito を使ったコンストラクターを mock 化する方法」について記載します。

Javaユニットテストを記述する際に利用するモックライブラリ mockito の使い方のひとつの説明です。

 

以前、以下で「mockito を使った static メソッドを mock 化する方法」について書きましたが、

その派生、コンストラクターを mock 化する場合の内容になります。

keyno63.hatenablog.com

 

コンストラクタを mock 化する際のモチベーションについて

この機能を使うモチベーション、つまり使いどころとしては、

メソッド内でクラスインスタンスを生成しているものがあり、

それをJUnit でテストする場合です。*1

 

具体的な例をコードで記述すると、以下のようなメソッドを想定しています。

public Object method(String value) {
var o = new SomeClass(value);
return o.doSomething();
}

 

mockito によるコンストラクタの mock 化

コンストラクタを mock 化する方法として powermock を使う方法があると思います。

前述した前回の記事でも触れましたが、powermock は JUnit5 対応や対応の検討もされていないため、

mockito を使った方法があるのでそれを使います。

 

mockito の mockConstruction というメソッドを使うと、

そのスコープ中は該当するクラスのインスタンスは自動的に mock に指し変わった状態となるため、

自由にふるまいを定義してテストに使うことができます。

 

Mockito 3.5.0 以降であれば、mockito によるコンストラクターの mock 化が可能になっています。

(出展として、草案のようなPR機能マージされたPRはありましたが、Github のリリースノートなどに機能追加されたことの説明などをみつけらませんでした。)

 

使い方の確認

実際の実装・動作確認した方法について触れていきます。

開発環境

  • OS: Windows 10 Pro
  • IDE: Intellij IDEA 2021.2 (Ultimate Edition)
  • JDK: openjdk-16.0.2
  • ビルドツール: gradle(7.0)

JDKバージョン、gradle のバージョンは古すぎなければほかのバージョンでも問題ないと思います。

私の環境では(たまたま)このバージョンを使っていました。

 

実装方法

mockito モジュールをビルドツールの設定に追加

gradle を使っている場合、build.gradle の依存関係に以下を追記します。

dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.0'
testImplementation 'org.mockito:mockito-inline:3.12.4'
testImplementation 'org.assertj:assertj-core:3.20.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.0'
}

test {
useJUnitPlatform()
}

モジュールのバージョンは記載時点(2021/09)の最新版を使用しています。

 

実装例

コンストラクターを使っているメソッドのテストの実装例を記載します。

以下のようなクラスをテストするとします*2

public class UseConstructorSample {

public String useMethod(String arg1, String arg2) {
ConstructorSample sample = new ConstructorSample(arg1);
return sample.method(arg2);
}

public static class ConstructorSample {

private final String value;

public ConstructorSample(String value) {
this.value = value;
}

public String method(String arg) {
return String.format("field=[%s], method=[%s]", value, arg);
}
}
}

ConstructorSample を mock 化し、UseConstructorSample の useMethod メソッドをテストすることを考えます。

 

実際にテストするコードは以下のように記述できます。

class UseConstructorSampleTest {

@Test
final void test() {
final String expected = "mockedValue";
try(MockedConstruction<ConstructorSample> mocked = mockConstruction(ConstructorSample.class,
(mock, ctx) -> doReturn(expected).when(mock).method(anyString()))
) {
UseConstructorSample target = new UseConstructorSample();
String actual = target.useMethod("value1", "value2");
assertThat(actual).isEqualTo(expected);
}
}
}

 

mockConstruction() メソッドの第一引数で mock 化したい対象のクラスを指定し、

第二引数で mock の振る舞いをラムダ式で定義しています。

第二引数で使っている mock が mock 化したい対象のクラス(今回だと ConstructorSample)になっているのに注意してください。

mockConstruction() が返す MockConstruction<T> のインスタンスは mock の制御をするためのインスタンスで、mock 自体ではないようです。

 

定義した mock の振る舞いは、try 文で囲われた領域で定義した通りの振る舞いを行います。

(mock, ctx) -> doReturn(expected).when(mock).method(anyString())

※doReturn、when を使って mock の振る舞いを定義。今回は「すべての文字列を受け取った時に expected を返す」振る舞いをする。

 

try 文で囲っているのは、MockConstruction を自動的に Close するためです。

インスタンスを Close されるまでが mockConstruction() で定義した mock の有効期間になります。

try 文を使わない場合は、最後に close() を呼ぶ必要があります。

MockedConstruction<ConstructorSample> mocked = mockConstruction(ConstructorSample.class);
...
mocked.close();

 

最後に

コンストラクターを mock 化する方法は、あまり使うケースが多くないとは思うのですが、

調べてみると mockito を使った方法についての情報になかなかたどりつかなかったり、

自分な必要な情報単位で巡り合えなかったので、このようにまとめるようにしました。

 

*1:可能であれば DI を使ったほうがテストのしやすさやインスタンスの一意性とかがあるのでメリットがあるのですが、事情によりこういうコードが紛れ込んでいる場合がありますよね

*2:ファイル分けすると視認性が悪くなるので内部クラスにしています