Mockitoとは
Mockitoは、Javaのユニットテストのために開発されたモックフレームワーク(mocking framework)です。テストでモックオブジェクトを直感的に操作できるのを目的として開発されています。
キレイでシンプルなAPIでモックを扱うテストコードを記述できること、また記述されたテストコードの可読性が高くわかりやすい検証エラーを生成することがMockitoの特徴です。
なぜモック化が必要なのか。
ユニットテストにおいてサービス単位に切り離してテストを行いたいにもかかわらず、処理内にサービスの呼び出しがあり、
サービス区切りにテストをすることが行えない場合があります。
そこで必要となるのがモック化です。
ユニットテスト時に依存関係ができているものをモック化することにより、正常にユニットテストを行うことができます。
モック化が想定されるのは以下のようなケースです。
・依存する部品で任意の内容をテスト対象に返すのが困難な場合
※たとえばHDDの容量不足というエラーを出力する必要がある試験の場合
・依存する部品を利用すると別の試験で副作用が発生する場合
※たとえばデーターベースの特定のテーブルを全て削除するような試験を行う場合
・依存する部品がまだ完成していない場合
※たとえばテスト対象のプログラムと依存する部品が並行で開発されている場合。
etc...
このような依存する処理やクラスをモックに置き換えることにより、単体テストを容易にします。
モックオブジェクトの種類
モックオブジェクトには、スタブやモックなどのオブジェクトが備わっています(厳密に言えば、モックの中にスタブが備わっている)。スタブやモックはオブジェクトですが、Mockitoではモックオブジェクトを介してスタブやモックの操作を行うためそれらを機能として捉えたほうがわかりやすいかもしれません。
テストを目的として本物のオブジェクトの動作を真似ているスタブやモックなどのオブジェクトの総称をテストダブルと呼びます。
テストダブルは5つの代役として、ダミー、フェイク、スタブ、モック、スパイを定義しています。
今回はスタブ、モック、スパイについて解説します。
- スタブ
メソッドの実行に対して、事前に定義された振る舞い(引数、返り値)を提供するオブジェクト。外部の依存性を排除し、テストがオブジェクトの実装の正しさだけに注目する目的で使われている。 - モック
メソッドの実行に対して、実行回数やパラメータの呼び出しを記録するオブジェクト。テスト対象から呼び出されたスタブの動作を記録し、その記録からメソッドの実行回数などを検証します。 - スパイ
メソッドの実行に対して、実行回数やパラメータの呼び出しを記録するオブジェクトです。モックと同様の役割ですが、基本的な定義として、モックはメソッドの実行中に検証するのに対して、スパイはメソッドの実行後に検証するという違いがあります。
Mockitoで扱う検証メソッドではメソッドの実行後に検証するので両者の違いはありませんが、Mockitoにおけるスパイはオブジェクトを部分的にモックする用途で使用されています。
インストール方法
mavenプロジェクトであればpom.xmlに以下の記載をします。
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>1.10.19</version>
</dependency>
どのようにmockオブジェクトを作成するのか?
// mockオブジェクトの作り方
// すでにモックオブジェクトが定義されている場合利用できる
private Service service = mock(Service.class);
@Mock
private Service service;
// テスト対象への注入
@InjectMocks
private Controller controller;
mockオブジェクトの作成には二つの方法があります。
①mock()メソッドを利用←初期化、インジェクトがいらない
②@Mockを利用してモック化したのち、初期化
上記に適宜されているように@Mockを利用してmock化をしたのちテスト対象にInjectします。
しかし、これだけでは利用できません。
利用するにはmockオブジェクトの初期化をしなければいけません。
Mockitoで言う初期化とは、モックオブジェクトを生成することなのです。
初期化処理はJUnitの@Beforeを利用してテスト前に行うように記述します。
@Before
private void initMocks() {
// @Beforeが付与されているメソッドのなかにinitMocksメソッドを書くことで、各テストメソッドを実行する前に毎回実行できる
MockitoAnnotations.initMocks(this);
}
振る舞いの定義方法
mockitoのオブジェクトのmockオブジェクトの作成方法はわかりました。
次はどのようにmockオブジェクトの振る舞いを定義するかを説明します。
モックオブジェクトのスタブメソッドの振る舞いを定義するのにthenメソッド系(thenReturnメソッドやthenThrowメソッドなど)とdoメソッド系(doReturnメソッドやdoThrowメソッド)があります。
whenメソッドとthenReturnメソッドもしくはdoReturnメソッドを合わせてスタブメソッドの振る舞いを定義します。それぞれのメソッド系で構文が異なります。
Mockito.when(モックインスタンス).メソッド(任意の引数).thenReturn(任意の戻り値);
// モックされたクラスのメソッドが呼ばれた時、この戻り値を返すよ
Mockito.doReturn(任意の戻り値).when(モックインスタンス).メソッド(任意の引数);
// この戻り値を返すよ、モックされたクラスのメソッドが呼ばれた時
どちらもできることに大差はありませんが、doメソッド系にしかできないことが2つあります。
- voidメソッドをスタブ化する場合
- @Spy、spyメソッドで生成したモックオブジェクトの場合
Mockito.doNothing().when(モックインスタンス).メソッド(任意の引数);
// 何も返さないよ、モッククラスのメソッドが呼ばれた時
このように戻り値がない場合もmock化することができます。
ちなみに、頭のMockito.は、あってもなくてもどちらでも大丈夫です。
PowerMockでも同じような書き方をするため、同時に使用する場合は使い分ける必要があるため、頭につけたほうが分かりやすくなると思います。
モック化したメソッドの呼び出し回数を検証する
メソッドをmockオブジェクトとした場合、何度実行されているのか確認したい場合があります。
その時に利用するのがverify()メソッドです。
Mockito.verify(モックインスタンス, times(index)).モックメソッド(引数);
verify()メソッドは第一引数にモックオブジェクト、第二引数にtimes()で引数に回数を持たせることにより想定の回数実行されているか確認を行うことができます。
よって戻り値が必要ないメソッドはverify()メソッドのみで実行確認ができ、戻り値の比較を行う必要がありません。
spyオブジェクトの使用法
spyオブジェクトは下記の二つの方法にて利用することができる。
//①
SampleController sampleController = spy(new SampleController());
//②
@Spy
@InjectMocks
SampleController sampleController = new SampleController();
spyオブジェクトはmockオブジェクトと異なり、内部の処理を残した状態でmock化することができる。
これによるメリットは、内部メソッドを一部mockオブジェクトとして利用できることにある。
privateメソッドとして切り分けたメソッドの処理が肥大化している場合、まだ一部処理要件が決まっていないなどといった場合においても問題なくテストが進められる点にある。
簡単な使用例
今回mockitを使用したテストコードを書くために下記ようなサービスとコントローラを用意しました。
@Component
public class MessageService {
public String getMessage(int a) {
if (1 < a) {
return "getMessageメソッドを呼び出しました。";
}
return "引数が小さいです";
}
public String margeMessage(String a, String b) {
return a + b;
}
}
@Controller
public class SampleController {
@Autowired
private MessageService messageService;
public String sample1(int i) {
return messageService.getMessage(i);
}
public String sample2(int i, String margeMessage) {
String message = messageService.getMessage(i);
message = messageService.margeMessage(message, margeMessage);
return messageTrim(message);
}
public String messageTrim(String message) {
if (2 < message.length()) {
return message.substring(0, 3);
}
return "文字数が小さいよ";
}
}
テストクラスは以下のように記述します。
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.*;
import org.junit.Before;
import org.junit.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import com.example.mocktest.controller.SampleController;
import com.example.mocktest.service.MessageService;
@ExtendWith(MockitoExtension.class)
public class SampleTest {
@Mock
MessageService messageService;
@Spy
@InjectMocks
SampleController sampleController = new SampleController();
@Mock
private final int i = 1;
@Mock
private final String margeMessage = "marge";
@Before
public void initMocks() {
// @Beforeが付与されているメソッドのなかにinitMocksメソッドを書くことで、各テストメソッドを実行する前に毎回実行できる
MockitoAnnotations.initMocks(this);
}
@Test
public void testcase001() {
// ServiceのgetMessageメソッドをモック化
doReturn("モック化できたよ").when(messageService).getMessage(i);
// 期待している返り値
String expected = "モック化できたよ";
// 実際の返り値
String actual = sampleController.sample1(i);
// 呼び出し回数確認
verify(messageService, times(1)).getMessage(i);
// 期待している返り値と実際の返り値を比較検証
assertThat(actual, is(expected));
}
@Test
public void testcase002() throws Exception {
// SampleController spy = PowerMockito.spy(sampleController);
// ServiceのgetMessageメソッドをモック化
doReturn("モック化できたよ").when(messageService).getMessage(i);
// ServiceのmargeMessageメソッドをモック化
doReturn("モック化できたよ").when(messageService).margeMessage("モック化できたよ", margeMessage);
// messageTrimモック化
doReturn("spyオブジェクトモック化").when(sampleController).messageTrim(anyString());
// 期待している返り値
String expected = "spyオブジェクトモック化";
// 実際の返り値
String actual = sampleController.sample2(i, margeMessage);
// 呼び出し回数確認
verify(messageService, times(1)).getMessage(anyInt());
verify(messageService, times(1)).margeMessage(anyString(), anyString());
verify(sampleController, times(1)).messageTrim(anyString());
// 期待している返り値と実際の返り値を比較検証
assertThat(actual, is(expected));
}
}
testcase001はSampleController.sample1のテストを行っている。MessageServiceをmockオブジェクトとして生成しテストを実行している。
これにより、Serviceの実装を考慮せず、Controllerの処理のみ確認するためのメソッドを作成できる。
testcase002はSampleController.sample2のテストを行っている。
このテストメソッドではSampleControllerをspyオブジェクトとして生成して、内部メソッドであるmessageTrimメソッドをmock化している。
これによりmessageTrimの内部処理を気にすることなくテストを実行できている。
anyInt()、anyString()は引数がIntまたはStringであれば良い(指定しない)場合に利用する。
voidメソッドの試験の場合利用すると紹介したdoNothing()も同様である。
また、privateメソッドをmock化する場合はPowerMockを利用する必要がある。
今回の記事では紹介することができなかったが、参考記事を参照に実践してみると勉強になると思います。
参考
[jmockit 1.48を使ってみる]
(https://qiita.com/mima_ita/items/4e48a24561960851e3fa)
[JMockit と共に生きる者ためのメモ]
(https://qiita.com/tonluqclml/items/5111ef8985c450bce4ca)
[Mockitoの最低限な使い方]
(https://qiita.com/fumu238/items/64c910bc36b051c6b976)
[【Java】Mockitoの飲み方(入門)]
(https://recruit.cct-inc.co.jp/tecblog/java/mockito-primer/)
[Mockitoを使ってみる。]
(https://qiita.com/yakumo3390/items/c34f1f1625c3beffaf9c)
[MockitoとPowerMockの使い分け]
(https://qiita.com/euledge/items/a224777c15ae7145114a)