おおくまねこ

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

mockito で static メソッドの mock を使ったテストをしたい

はじめに

お久しぶりです。

最近、いろいろバタバタしていてここの更新もすすんでませんでしたが、

アウトプットの機会を継続できたらと思い、また何か書いていこうとしています。

4月に異動になってしまい、Java を使わない部署になったのに、なぜかスポット対応でJavaを書いていました。

今日はその時に見つけた内容の話になります。

内容としては「mockito と使った、static メソッドを mock 化する方法」です。

 

static メソッドの mock 化するモチベーション

mock 化したいのは、おもにユニットテストを書く際に、依存モジュールを好きな値をかえす、mock/stub を使ってテストしたいとなると思います。

 

Java で実装を行う場合、自分たちで static メソッドを作る場合もあると思います。

自分たちで作らない場合でも外部モジュールで実装された static メソッドを使ったりする場合もあると思います。

そういったものに依存するクラスを開発することになった場合、

ユニットテストを書こうとした際、mock 化するには

どのようにすると良さそうかということを考えます。

 

powermock

方法のひとつとして、 powermock を使ったテストというのが選択肢に入ってきたと思います。

ただ、どうも JUnit5 の対応がされず、何年も時間が経過してしまっているようでした。

github.com

※類似の内容の issue が他にも複数あがっています

 

JUnit4 であれば powermock 使えるのですが、

SpringBoot の test ライブラリはデフォルトで JUnit5 になっていたり、

そもそも PowerMock 使ってないようなレポジトリーはすでに JUnit5 を使っていたりするので、

powermock 使うためだけに JUnit4 を使うというのはあまりうれしくないなと思っていました。

 

代替案としての mockito の使用

そこで JUnit5 で使えて、static メソッドを mock 化できる方法はないかと調べたところ、

mockito 3.4 以降であれば、そういうことができるようになっていました。

実際に試したところ、うまく動かすこともできたので、やった内容も記載しておきたいと思います。

 

開発環境

開発環境する際に使った環境について。

Java なのであまり開発環境には依存しないと思いますが、

JDKやビルドツールについても一応記載します。

環境は以下になります。

 

実装方法

実装方法について記載していきます。

おおまかな流れは以下の通りです。

  • ビルドツールの依存関係に mockito-inline を追加する
  • テストコードのクラス作成
  • static メソッドをもつクラスの mock 化
  • mock の挙動定義
  • テストの実装

(プロジェクトの作成や、ロジックコード側はすでにあるものと思っています。)

mockito モジュールの追加

ビルドツール(今回は gradle )に、必要なモジュールを追記します。

私の場合は build.gradle に以下を記載しました。

dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0'
testImplementation 'org.mockito:mockito-inline:3.10.0'
testImplementation 'org.assertj:assertj-core:3.19.0'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
}

test {
// Junit5 を使う設定
useJUnitPlatform()
}

ライブラリのバージョンは当時の最新版だったり、なるべく新しいのを選びました。

 

mockito-inline というモジュールが、実際に static メソッドの mock 化を行ってくれるようなので、これを取り込む必要があります。

mockito-core は指定しなくても、mockito-inline が mockito-core を引っ張ってくれるので記述しなくても問題ありません。

(何らかの理由でバージョンのオーバーライドとかしたくて記述しても動作はするとおもいます。)

 

テストコードのクラス作成~テストの実装

実際のテストを実行するクラスを作成します。

前提条件で「すでにプロジェクトのロジックコードは存在する」としましたが、

今回は以下のようなクラスがあるとしてください。

  • static メソッドを持つクラス(StaticSample)
  • static メソッドを持つクラスに依存するクラス(UseStaticSample)
  • UseStaticSample をテストするためのテストクラス(UseStaticSampleTest)

 

ロジックコードとしては以下を実装します。

  • StaticSample
import java.util.Objects;

public class StaticSample {

public static String value1() {
return "value1";
}

public static boolean isEqual(String value1, String value2) {
return !Objects.isNull(value1) && value1.equals(value2);
}
}
  • UseStaticSample
public class UseStaticSample {

private final String value;

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

public String function(String value) {
final boolean isEquals = StaticSample.isEqual(this.value, value);
if (isEquals) {
return String.format("Arg value is equal, value =[%s]", value);
} else {
return "Not Equal";
}
}
}

 

実際に、それらのクラスをテストするUseStaticSampleTest は以下のようになります

  • UseStaticSampleTest
@SuppressWarnings("NonAsciiCharacters")
class UseStaticSampleTest {

private UseStaticSample target;
private static final String INIT_VALUE = "value";

@BeforeEach
void setUp() {
target = new UseStaticSample(INIT_VALUE);
}

@Test
final void test() {
final String argValue = "someValue";
final String expected = String.format("Arg value is equal, value =[%s]", argValue);

try(MockedStatic<StaticSample> mocked = mockStatic(StaticSample.class)) {
mocked.when(() -> StaticSample.isEqual(anyString(), anyString()))
.thenReturn(true);

String actual = target.function(argValue);

assertThat(actual).isEqualTo(expected);
}
}
}

 

これを実行すると、UseStaticSample 内で使っている、StaticSample のメソッドの挙動が書き換わり、

他のクラスに依存しないテストコードを書くことができました。

テストの実装について説明

テストの実装までの実装した内容についても触れておきたいと思います。

mock インスタンスの生成について

org.mockito.MockedStatic メソッドで、static メソッドを持つクラスの mock インスタンスを生成します。

MockedStatic<StaticSample> mocked = mockStatic(StaticSample.class)
mock インスタンスのライフサイクルについて

mockStatic() によって mock を生成した場合、最後に close() を呼んでmock を停止させる必要があります。

// create mock
MockedStatic<StaticSample> mocked = mockStatic(StaticSample.class);
mocked.when(() -> StaticSample.isEqual(anyString(), anyString()))
.thenReturn(true);

String actual = target.function(argValue);
assertThat(actual).isEqualTo(expected);

// close mock
mocked.close();

 

これをしないと、他のテストで mock 化しようとした際にエラーとなってしまうためです。

ただ、上述したような、try-with-resource の形で、

try で受けてあげれば、try 句を抜ける際にいっしょに close してくれます。

 // create mock
try(MockedStatic<StaticSample> mocked = mockStatic(StaticSample.class)) {
mocked.when(() -> StaticSample.isEqual(anyString(), anyString()))
.thenReturn(true);

String actual = target.function(argValue);
assertThat(actual).isEqualTo(expected);

// close mock
}

 

mock のふるまいの定義

mock のふるまいについて定義します。

これは通常の mock 化された mock インスタンスと同じ要領で、when や thenReturn で挙動を決めています。

その時に定義する、呼び出されるメソッドはラムダ式で記述します。

mocked.when(() -> StaticSample.isEqual(anyString(), anyString()))
.thenReturn(true);

※StaticSample.isEqual() のふるまい定義。すべての文字列を引数に受けたときに true を返してほしい。

 

ラムダ式なので、引数のない場合はメソッド参照の形で記述することもできます。

引数のない、StaticSample.value1() の場合は以下のどちらかになります

  • メソッド参照の場合
mocked.when(StaticSample::value1)
.thenReturn("anyString");
mocked.when(() -> StaticSample.value1())
.thenReturn("anyString");

 

補足

mockito でコンストラクタの mock する方法について、以下で説明しました。

keyno63.hatenablog.com

最後に

いままで static メソッドのテストコード作成には結構悩まされてきましたが、

mockito で static メソッドのテストができるようになってくれたおかげで

テストライブラリのバージョン関係やテストの記述方法に関する問題が解消できたのが

非常に大きな収穫になりました。

 

もしこの記事を見かけた方で試したことのない方は是非お試しいただければと思います。

 

追記

コンストラクタを Mockito で mock する方法についても書きました

keyno63.hatenablog.com