新しい会社ではきちんとユニットテストを書く文化があったので、練習のためMockitoの利用方法を練習していました。
今まで経験した会社ではユニットテストを作る文化がなかったので、仕事でかなり単体テスト作成に手間取っています。
少しでもこの遅れを取り戻したい!ということもあり、Mockitoの練習をしてみました
今回のプログラム
今回のサンプルプログラムは以下になります
https://github.com/sheltie-fusafusa/JUnitMockitoPractice
この記事では、上記プログラム内で個人的に詰まった部分を抜粋して紹介します
そもそもMockitoとは?
今回始めてMockitoに触ったのですが、そもそもこれが何なのか?何のために必要なのか?もわからない状態でした。
軽く調べて、以下のように理解しました。
Mockitoとは
 プログラムのモックを簡単に作成するためのフレームワーク。Mock化したいクラスを指定して、各メソッドが呼び出されたときの戻り値を指定することでテストが行える
なぜMock化が必要なのか?
 プログラムは単体ではなく、別のシステム(DBやAPI)やライブラリなどの他パーツを組み合わせて作成される。ある機能のテストを行おうとした時、DBに処理に使用するデータの登録が無かったり、連携先の外部APIが未完成だと、当然テストが行えない。そこでDBから値を返す処理や、外部APIと連携する部分をMock化することでテストの都合の良いデータが返却できるようになり、テストが行えるようになる
と、理解しました
シンプルなクラスで、テストを行ってみる
今回は以下のようなクラスを作成しました
package org.example.service;
public class ExampleService {
    public int plus(int a, int b){
        return a + b;
    }
    public int square(int a){
        return a * a;
    }
    public int minus(int a, int b){
        return a - b;
    }
    public int divide(int a, int b){
        if(b == 0){
            return 0;
        }
        return a / b;
    }
    // 足し算した後に2乗する
    public int plusSquare(int a, int b){
        int result = plus(a, b);
        return square(result);
    }
}足し算や引き算、累乗を計算するようなシンプルな内容のクラスです。
このクラスに対して単体テストを記述すると、以下のようになります
package org.example.service;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class ExampleServiceTest {
    @Test
    public void plusTest(){
        // プラスメソッドが正しく動くことを確認
        ExampleService exampleService = new ExampleService();
        int result = exampleService.plus(1, 2);
        assertEquals(result, 1 + 2);
    }
    @Test
    public void plusSquareTest(){
        // モック準備
        ExampleService exampleServiceMock = mock(ExampleService.class);
        when(exampleServiceMock.plus(anyInt(), anyInt())).thenReturn(10);
        // 上記でplusのMockを作成し、必ず10を返却するように設定しているため
        // 以下の処理では2ではなく、10が返却される
        int result = exampleServiceMock.plus(1,1);
        assertEquals(10, result);
    }
}基本的なテスト方法は
・テストしたいメソッドを呼び出す
・assert〜メソッドで値を比較する
の流れになります。
plusSquareTestメソッドで実際にMockを作成し、plusメソッドで 10を返却するように設定しています。そのため、plus(1,1)の結果は2ではなく10が返却され、assertEquals(10, result);の結果、テストOKという動きとなります
問題はstaticメソッドのテスト
Mockitoではstaticメソッドのテストも3.4.0からサポートされたとのこと。
以下のようにsutaticメソッドを多数含むクラスを作成し、上記と同様のテストを作成してみました
package org.example.service;
public class StaticExampleService {
    public static int plus(int a, int b){
        return a + b;
    }
    public static int square(int a){
        return a * a;
    }
    public static int minus(int a, int b){
        return a - b;
    }
    public static int divide(int a, int b){
        if(b == 0){
            return 0;
        }
        return a / b;
    }
    // 足し算した後に2乗する
    public static int plusSquare(int a, int b){
        int result = StaticExampleService.plus(a, b);
        return StaticExampleService.square(result);
    }
}
テストはこんな感じです
package org.example.service;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.*;
public class StaticExampleServiceTest {
    @Test
    public void staticPlusTest(){
        try(MockedStatic<StaticExampleService> mocked = mockStatic(StaticExampleService.class)){
            // モックのセットアップ
            mocked.when(() -> StaticExampleService.plus(anyInt(), anyInt())).thenReturn(3);
            int result = StaticExampleService.plus(10, 20);
            // plusメソッドはMock化されており、どのような値でも3で返却するように設定されている
            assertEquals(3, result);
        }
    }
    @Test
    public void staticPlusSquareTest(){
        try(MockedStatic<StaticExampleService> mocked = mockStatic(StaticExampleService.class)){
            // モックのセットアップ
            mocked.when(() -> StaticExampleService.plus(anyInt(), anyInt())).thenReturn(3);
            int result = StaticExampleService.plusSquare(2,2);
            // plusメソッドはMock化されており、3が返却されるので9になるはず
            assertEquals(9, result);
        }
    }
}スタティックメソッドの場合、mockStaticメソッドを利用してモックを作成します
また、try-with-resourceを利用することで、mocked.close()開放漏れを防ぐ事ができます
参考:https://keyno63.hatenablog.com/entry/2021/05/15/060156
staticPlusTestについては問題なくテストを行うことが出来ました。
問題はstaticPlusSquareTestで、上記の記述では正しく動きません
staticPlusSquareTestの問題点とは
staticメソッドの中でローカル変数を利用している場合、テストが正しく動かないようです


上記の通り、ローカル変数に設定された値が読み込まれず、plusメソッドは3を返却することを期待してMockを構築下にも関わらず、0が返却されテストが失敗します。
このあたりの解決法を土日に調べてみたのですが、このように動作する理由や解決法は見つかりませんでした・・・
以上
ということで、JUnitとMockitoの利用方法を土日に調べていました。というお話でした。
結果的に解決出来ずで心残りはありますが、もうだめなのでここで一旦打ち切りにします。
上記のことから、基本的にstaticメソッドはモック化しないほうが良い気がしていますが、何かしら、ユニットテストのベストプラクティを知りたいところです。
こっそりStackOverFlowで聞いてみようかしら
 


