【JUnit5】JMockitoを利用してMock化する

テストコードを書く際に、モックして書くことがあります。
モック用のライブラリには「Mockito」等がありますが、今回は「JMockito」学ぶ機会があったのでこちらを利用してモック化してみたいと思います。

Mock(モック)

モック化することでテスト対象が依存するクラス、メソッドの振る舞いを定義することが可能です。
(似たような言葉としてMockの他にSpy,Stubという言葉があります)

モック化するメリットとしては以下のようなことが挙げられると思います。
・依存するクラスが存在していたとしてもMock化することでテストコードが書ける
・外部に依存しないテストが可能
・モック化したメソッドの振る舞いに対してテスト結果が正しいかを確認することが可能
・モックしたメソッドの引数・実行回数をチェックできる

【イメージ】
下記のメインクラスではCalcクラスを生成してdoubleNumメソッドを利用しています。
実際の実装は引数の値を2倍にするものです。

public class Main {

	public static void main(String[] args) {
		
		int a = 0;
		
		Calc cal = new Calc();
		// 引数の値を2倍にして返却
		int result = cal.doubleNum(a);
		
		System.out.print(result);

	}
}

Calcクラスをモック化するとイメージとしては下記のようになります。

実装は2倍にするメソッドですが、モック化することで実装が空になり自身で振る舞いを定義する必要があります。3倍にして返却と設定しています。
正常に値を返却するではなくエラーを発生させることも可能です。なのでエラーを発生させてのテストも簡単にすることが可能です。

前提

今回テスト対象とクラスは「DiaryService.java」という自作したクラスになります。
このクラス内で実施しているdaoクラスをモック化していきます。
※内部の処理は気にしなくて大丈夫です。

    public List<Diary> findList(GetForm form) {
        return dao.findList(form);
    }

<Diary>・・・データを格納するようのEntityクラス
dao.findList(form)・・・formクラスに格納された条件に合致するデータ取得するクラス
            (DBよりSELECT文を実行してデータを取得)
             daoはInterfaceでも受け取れるようにしている

参考までにこちらのクラスは下記で作成したものとなります。

実装①(Expectations,Verifications)

モック化したクラスを利用してテストコードを記載したいと思います。
定義するために「Expectations」を利用します。
Mock化したクラスのパラメータや呼び出し回数をチェックするために「Verifications」を利用します。

package diary;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.Arrays;
import java.util.List;

import org.junit.jupiter.api.Test;

import diary.entity.Diary;
import diary.form.GetForm;
import diary.repository.IDiaryDao;
import diary.service.DiaryService;
import mockit.Expectations;
import mockit.Mocked;
import mockit.Verifications;

public class DiaryServiceMockTest {

	// モック化
	@Mocked
	IDiaryDao dao;

	@Test
	void 正常() {

		// 振る舞いを定義
		new Expectations() {{
			dao.findList((GetForm) any);
			List<Diary> list = Arrays.asList(new Diary(), new Diary(), new Diary());
			result = list;
		}};

		// パラメータ設定
		GetForm form = new GetForm();
		form.setCategory("1");
		// テスト対象クラスをインスタンス化
		DiaryService service = new DiaryService(dao);
		// 実行
		List<Diary> result = service.findList(form);

		// Mockクラスの検証
		new Verifications() {{
			// パラメータ格納用
			GetForm paramCheck = new GetForm();
			// 実行回数チェックのため記載 + withCapture()にてパラメータ取得
			dao.findList(paramCheck = withCapture());
			// 実行回数確認
			times = 1;
			// パラメータチェック
			assertEquals("1", paramCheck.getCategory());
		}};

		// findListメソッド戻り値確認
		assertEquals(3, result.size());
	}
}
ポイント
@Mocked
IDiaryDao dao;

daoクラスをモック化しています。

new Expectations() {{
  dao.findList((GetForm) any);
  List list = Arrays.asList(new Diary(), new Diary(), new Diary());
  result = list;
}};

Expectations()でモック化したクラスの振る舞いを定義することができます。
※モッククラスが複数あっても定義することが可能
result で結果の値を定義しています。Entitiyクラスが3つ格納されたリストが実行結果となります。引数にanyと記載していますが、任意の値という意味になります。

new Verifications() {{
		// パラメータ格納用
		GetForm paramCheck = new GetForm();
		// 実行回数チェックのため記載 + withCapture()にてパラメータ取得
		dao.findList(paramCheck = withCapture());
		// 実行回数確認
		times = 1;
		// パラメータチェック
		assertEquals("1", paramCheck.getCategory());
	}};

	// findListメソッド戻り値確認
	assertEquals(3, result.size());
}

Verifications()でモック化したクラスの実行回数、パラメータチェックを実施しています。
dao.findList(paramCheck = withCapture());」を記載することで実行回数をチェックできます。その後ろに記載している「times = 1」でこのメソッドが1回実行されること、というチェックになります。
withCapture()」でモックメソッドのパラメータを取得することが可能です。
パラメータを取得し、「assertEquals」でチェックしています。
※パラメータが複数存在する場合はparamCheck変数をリスト型にすることで取得可能
最後に「assertEquals(3, result.size());」を記載し、実行結果の返却値のリストが3つ要素が存在することを確認しています。振る舞いを定義した中でEntitiyクラスが3つ格納されたリスト結果にしているので要素数は3となります。

実装②(new MockUp)

今度はモック化したクラスの定義を設定するのではなく、既にインスタンスが作成されたクラスの一部メソッドの振る舞いを変更したいと思います。
実装①ではモック化し空になったクラスの振る舞いを定義するために「Expectations()」を利用しました。実装②ではモック化していないクラスの一部メソッドの振る舞いを変更(差し替える)することができるのでそれを実装していきます。

テスト対象のメソッドで呼び出している同じクラス内の別メソッドをモック化したい場合などに用いられます。このパターンではないですが、実装してみます。

package diary;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.Arrays;
import java.util.List;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import diary.entity.Diary;
import diary.form.GetForm;
import diary.repository.DiaryDao;
import diary.service.DiaryService;
import mockit.Mock;
import mockit.MockUp;

@SpringBootTest
public class DiaryServiceMockTest2 {

	// テストクラス 依存するクラスをインスタンス化
	@Autowired
	DiaryService service;

	@Test
	void 正常() {
		new MockUp<DiaryDao>() {
			@Mock
			public List<Diary> findList(GetForm form) {
				// パラメータチェック
				assertEquals("1", form.getCategory());
				// 戻り値設定
				List<Diary> list = Arrays.asList(new Diary(), new Diary(), new Diary());
				return list;
			}
		};

		// パラメータ設定
		GetForm form = new GetForm();
		form.setCategory("1");
		// 実行
		List<Diary> result = service.findList(form);
		// findListメソッド戻り値確認
		assertEquals(3, result.size());
	}
}
見出し
@Autowired
DiaryService service;

Springの機能を利用して、Serviceクラスとそれに依存するクラスをインスタンス化しています。

new MockUp<DiaryDao>() {
  @Mock
  public List findList(GetForm form) {
    // パラメータチェック
    assertEquals("1", form.getCategory());
    // 戻り値設定
    List list = Arrays.asList(new Diary(), new Diary(), new Diary());
    return list;
  }
};

findListメソッドの振る舞いを定義しています。
実行時は実際の実装と差し変わるのでこちらの振る舞いになります。
MockUp内でパラメータチェックもすることが可能です。
実装①と違い既にインスタンスが生成されているため<DiaryDao>クラスを指定しています。
IDiaryDao:interface
DiaryDao:クラス

最後に

モック化することで他クラスに依存しないでテストすることが可能となります。
ここで紹介したこと以外にもモック用のライブラリを使用することで様々なことができます。

テストコードを書けるだけでなく、どういう方針でテストコードを書くのが決めれるようになりたいな〜

参考文献

The JMockit testing toolkit
Home of the JMockit open source project. JMockit is a Java toolkit for developer testing, including mocking APIs and a code coverage tool.
JMockitテストツール 非公式日本語訳
JMockit オープンソースプロジェクトのホーム. JMockitはモックAPIやカバレッジツールを含む開発者向けのJavaテストツールキットです.
タイトルとURLをコピーしました