はじめに
こんばんわ、きりです。
本記事はNablarchを使ってみようのサブ記事として作成しております。
前回のJUnitを使ったテストの実施方法では、EclipseにおけるJUnitの利用方法について整理しました。
JUnitを利用することで、実装コードのテストおよび、将来的なデグレートチェック(リグレッションテスト)に利用できることがわかりました。
より実践的にテストを実施するために、Mockitoというライブラリの利用方法について整理しようと思います。
JUnitのバージョンについては、保守開発ではバージョン4が使われているケースが多いかと思いますが、本記事では最新のバージョン5を利用します。
本記事以外のコンテンツはこちらから閲覧可能です。
なるべく、初心者目線で作成するつもりですが、分かりづらい部分ありましたら、コメント頂きたいです。
動作環境
種類 | バージョン |
---|---|
OS | Windows Professional 20H2 |
Eclipse | 2022 Full Edition |
※Eclipseのダウンロードについては、こちらの記事をご確認ください。
プロジェクトの作成
以下の構成でMavenプロジェクトを作成します。
キー | 値 |
---|---|
アーキタイプグループId | org.apach.maven.archetypes |
アーキタイプアーティファクトId | maven-archetype-quickstart |
グループId | com.kiri |
アーティファクトId | mock-sample |
バージョン | 0.0.1-SNAPSHOT |
パッケージ | com.kiri.mocksample |
※Mavenを使ったプロジェクトの作成については、EclipseにおけるMavenを使ったシンプルプロジェクトの作成と実行を参考にしてください。
※App.javaおよびAppTest.javaは今回利用しませんので、削除してください
JREとコンパイラーのバージョンが1.7になっているので、最低でも8以上に変更します。
※変更方法はEclipseにおけるMavenを使ったシンプルプロジェクトの作成と実行を参考にしてください。
Mockitoの利用に必要なライブラリーをダウンロードする
pom.xmlファイルの編集
pom.xmlファイルを開いて、以下の要素を追加します。
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<!-- 以下を追加 -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>4.4.0</version>
<scope>test</scope>
</dependency>
<!-- ここまで追加 -->
</dependencies>
プロジェクトを更新
プロジェクト名>Maven>プロジェクトの更新 をクリックします。
追加した「mockito-core」と「mockito-junit-jupiter」およびその依存ライブラリがダウンロードされます。
この作業により、ソース内で「mockito-core」と「mockito-junit-jupiter」内のクラスをimportすることが可能となります。
利用ケース1(@Mock使用)
AクラスとBクラスを作成し、AクラスでBクラスをインスタンス化して利用する構成において、
Aクラスから呼び出すBクラスのメソッドの戻り値をテスト用に自由に変更したい場合。
Bクラスの内部実装などによらず、Aクラスとして想定しているBクラスメソッドの戻り値を自由に設定したい場合。
テスト対象のクラスを作成
DisplayUserInfoとUserInfoを作成し、DisplayUserInfoからUserInfoのインスタンスを生成しメソッドを呼び出すような、
テスト用のコードを作成します。
パッケージの追加
プロジェクト作成時に「com.kiri.mocksample」というパッケージを作成しました。
用途に合わせて、「com.kiri.mocksample.beans」と「com.kiri.mocksample.case1」というパッケージを追加します。
以下は「com.kiri.mocksample.beans」の追加方法です。
パッケージ・エクスプローラにて、プロジェクト名を右クリック>新規>パッケージを選択します。
名前に「com.kiri.mocksample.beans」を入力して、完了ボタンをクリックします。
もともと、com.kiri.mocksampleパッケージの子要素として「beans」が追加されます。
上記と同様の手順で、「com.kiri.mocksample.case1」を追加します。
クラスの追加
2つのクラスを作成します。
- 「com.kiri.mocksample.beans」パッケージにUserInfoクラス
- 「com.kiri.mocksample.case1」パッケージにDisplayUserInfoクラス
以下、UserInfoクラスの追加手順となります。
パッケージ・エクスプローラにて、プロジェクト名を右クリック>新規>クラスをクリックします。
パッケージ名に「com.kiri.mocksample.beans」、名前に「UserInfo」を入力して、完了ボタンをクリックします。
「com.kiri.mocksample.beans」パッケージにUserInfo.javaが作成されました。
上記と同様の手順で、DisplayUserInfoクラスを作成します。
クラスの編集
UserInfoクラスはユーザーの名前と年齢と性別を管理するクラスを想定しています。
DisplayUserInfoクラスはUserInfoから情報を取得してアウトプットするようなクラスを想定していましたが、
今回は出力する文字列を返却するメソッドを作成しました。
package com.kiri.mocksample.case1;
import com.kiri.mocksample.beans.UserInfo;
public class DisplayUserInfo {
private UserInfo info = new UserInfo();
public String getUserInfoString(String userId) {
String name = info.getName(userId);
String gender = info.getGender(userId);
String old = info.getOld(userId);
return name + "(" + gender + ") " + old + "歳";
}
}
package com.kiri.mocksample.beans;
// UserInfoは現在開発中。という体。
public class UserInfo {
public String getName(String id) {
String name = "デフォルト";
// 実装中
return name;
}
public String getGender(String sexId) {
String gender = "デフォルト";
// 実装中
return gender;
}
public String getOld(String sexId) {
String old = "デフォルト";
// 実装中
return old;
}
}
テストケースの作成
テストケースの作成手順については、EclipseにおけるJUnit5を使ったテストの実施方法の記事を御覧ください。
DisplayUserInfoTestクラスを作成し、以下のように編集します。
package com.kiri.mocksample.case1;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import org.junit.After;
import org.junit.Before;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.jupiter.MockitoExtension;
import com.kiri.mocksample.beans.UserInfo;
@ExtendWith(MockitoExtension.class) // JUnit5でMockito使うには必要
public class DisplayUserInfoTest {
@Mock
private UserInfo mockInfo;
private AutoCloseable closeable;
@InjectMocks
private DisplayUserInfo target;
@Before
void initService() {
closeable = MockitoAnnotations.openMocks(this);
}
@After
void closeService() throws Exception {
closeable.close();
}
@Test
public void testMock() {
when(mockInfo.getName("000001")).thenReturn("鈴木一郎");
when(mockInfo.getGender("000001")).thenReturn("男");
when(mockInfo.getOld("000001")).thenReturn("42");
String result = target.getUserInfoString("000001");
assertEquals("鈴木一郎(男) 42歳", result);
}
}
ソースについて解説します。
今回のケース(UserInfoのメソッド戻り値を自由に指定する)で行いたいことは
モック化したクラス(今回はUserInfo)のインスタンスを作成し、テスト対象となるクラス(今回はDisplayUserInfo)のインスタンスを注入することです。
その後、モック化したクラスのメソッドに対して、戻り値のルールを作成することで自由に戻り値を変更することができます。
モックの作成はインスタンス変数の宣言に@Mockを記入します。
モックの注入は注入先のインスタンス変数宣言の前に@InjectMocksを記入します。
@Mockと@InjectMocksによる注入処理は、MockitoAnnotations.openMocks(this)呼び出し時に行われます。
MockitoAnnotations.openMocks(this)で作成されたリソースは、closeメソッドによって行われます。
そのため、 @Before(もしくは@BeforeEach)でMockitoAnnotations.openMocks(this)を呼び出し、@After(もしくは@AfterEach)でcloseメソッドを呼び出します。
MockitoAnnotations.openMocks(this)はインスタンス内に@Mockアノテーションを探し、発見したインスタンスをモックとして初期化します。
また、MockitoAnnotations.openMocks(this)の戻り値として返却されるAutoCloseable型のインスタンスのcloseメソッドを呼び出すことで、
リソースが解放されます。
次に戻り値のルールを作成をwhen関数を使って行います。
when(モックインスタンス.ルールづけするメソッド(ルール付けする引数値)).thenReturn(返却させたい戻り値);
今回の例では、getNameメソッドに"000001"という変数が指定された場合は"鈴木一郎"を返却するようルールを作っています。
when(mockInfo.getName("000001")).thenReturn("鈴木一郎");
また、引数の値によらず戻り値を変更する場合は以下のようにanyStringメソッドを使用する。
when(mockInfo.getName(anyString)).thenReturn("鈴木一郎");
target.getUserInfoStringメソッドの戻り値がモックのルールづけにより、
"鈴木一郎(男) 42歳"となり、テストが成功しました。
補足1
今回
when(mockInfo.getName("000001")).thenReturn("鈴木一郎");
した部分は、
doReturn("鈴木一郎").when(mockInfo).getName("000001");
とすることも可能。
今のところ、好みで使い分ける程度の違いと理解しています。
補足2
今回
when(mockInfo.getName("000001")).thenReturn("鈴木一郎");
した部分は、
when(mockInfo.getName(anyString())).thenReturn("鈴木一郎");
とることもできます。
違いは、"000001"を引数にgetNameを呼び出した場合のみ"鈴木一郎"を返却するか、任意の文字列を引数にgetNameを呼び出した場合に"鈴木一郎"を返却するかの違いです。
テストの目的に応じて、使い分けるべきものと認識しています。
このコードの違いを見ると、今度は
when(mockInfo.getName("000001")).thenReturn("鈴木一郎");
で、"000001"以外を引数にした場合も挙動が気になりますよね。
次節で試してみます。
補足3
試しに
when(mockInfo.getName("000001")).thenReturn("鈴木一郎");
を
when(mockInfo.getName("000002")).thenReturn("鈴木一郎");
とし、テストを実施してみます。
怒られました。
利用ケース2(@Spy使用)
利用ケース1と同じことを今度はMockではなくSpyを使って行います。
※違いは後述
テスト対象のクラスを作成
利用ケース1で作成したクラスを使い回します。
クラスの編集
利用ケース1で作成したクラスを使い回します。
テストケースの作成
利用ケース1で作成したDisplayUserInfoTestクラスを、以下のように編集します。
package com.kiri.mocksample.case1;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import org.junit.After;
import org.junit.Before;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import com.kiri.mocksample.beans.UserInfo;
@ExtendWith(MockitoExtension.class) // JUnit5でMockito使うには必要
public class DisplayUserInfoTest {
// モック化するクラスのインスタンスを生成します。
@Spy
private UserInfo mockInfo;
private AutoCloseable closeable;
// モックを注入するクラスのインスタンスを生成します。
@InjectMocks
private DisplayUserInfo target;
// this(mockInfo)を初期化します。
@Before
void initService() {
closeable = MockitoAnnotations.openMocks(this);
// MockitoAnnotations.initMocks(this);
}
@After
void closeService() throws Exception {
closeable.close();
}
// ここがテストの中身
@Test
public void testMock() {
// ここでモックを作成しています。
when(mockInfo.getName("000001")).thenReturn("鈴木一郎");
when(mockInfo.getGender(anyString())).thenReturn("男");
doReturn("42").when(mockInfo).getOld("000001");
// テスト対象のクラスを実行します。
String result = target.getUserInfoString("000001");
// 戻り値を確認する。
assertEquals("鈴木一郎(男) 42歳", result);
}
}
大きな違いとして、@Mockを@Spyに変更しました。
結果は変わらなず、テストが成功すると思います。
@Mockと@Spyの違い
@Mockが指定したインスタンス内のメソッドをすべてモック化し、@Spyは指定したメソッドのみモック化するという違いがあります。
@Mockのおいて、when関数やdoReturn関数にて、戻り値を指定しない場合はそのメソッドは何もしない状態となります。
具体的に試してみます。
ケース1のDisplayUserInfoTestクラスのうち
when(mockInfo.getName("000001")).thenReturn("鈴木一郎");
when(mockInfo.getGender("000001")).thenReturn("男");
when(mockInfo.getOld("000001")).thenReturn("42");
の部分をコメントアウトしてテストを実行すると、
以下の結果のように、該当メソッドの部分がnullとなり、何も処理されていないことがわかります。
次に、@Spyの場合です。
同じくケース2の
when(mockInfo.getName("000001")).thenReturn("鈴木一郎");
when(mockInfo.getGender(anyString())).thenReturn("男");
doReturn("42").when(mockInfo).getOld("000001");
の部分をコメントアウトしてテストを実行すると、
以下の結果のように、メソッドが呼び出されていることがわかります(各メソッドは現状、”デフォルト"を返却する実装となっている)。
@Mockと@Spyの使い分けは、テストの目的や対象のクラスの性質により行うものかと思います。
全てのメソッドを置き換える前提の場合は、@Mockを使用することで置き換え漏れに気づけると思いますし、
一部のメソッドのみを置き換えてテストしたい場合は、@Spyを利用すればよいかなと思いました。
利用ケース3(戻り値の型がvoidメソッドへの対応について)
メソッドにはStingやintのような戻り値を返却するものと、何も戻り値を返却しないものがあります。
いわゆるvoidメソッド(関数)です。
※個人的にvoidメソッドってあまり聞かないですが。。。
まず、voidメソッドに対してはwhen関数やdoReturn関数が利用できません。
戻り値が存在しないためです。
代わりというわけではないですが、voidメソッドに対しては、以下2点の方法にて検証を行います。
- verify関数による呼び出し回数の検証
- doNothingによるメソッドの無効化
クラスの編集
まずは、voidメソッドの確認のために、ケース1のクラスを編集します。
UserInfoクラスにvoidメソッド(voidFunction)を追加します。
package com.kiri.mocksample.beans;
// UserInfoは現在開発中。という体。
public class UserInfo {
public String getName(String id) {
String name = "デフォルト";
// 実装中
return name;
}
public String getGender(String sexId) {
String gender = "デフォルト";
// 実装中
return gender;
}
public String getOld(String sexId) {
String old = "デフォルト";
// 実装中
return old;
}
public void voidFunction(String id) {
System.out.println("Hello, World!");
return;
}
}
次にDisplayUserInfoでvoidFunctionを呼び出す処理を追加します。
package com.kiri.mocksample.case1;
import com.kiri.mocksample.beans.UserInfo;
public class DisplayUserInfo {
private UserInfo info = new UserInfo();
public String getUserInfoString(String userId) {
String name = info.getName(userId);
String gender = info.getGender(userId);
String old = info.getOld(userId);
// voidメソッドの呼び出し(当然戻り値はない)
info.voidFunction(userId);
return name + "(" + gender + ") " + old + "歳";
}
}
verifyによる呼び出し回数の検証
verify関数を利用することで、指定したモックインスタンスのメソッドが呼び出されたかを検証することができます。
テストケースを以下のように編集します。
package com.kiri.mocksample.case1;
import static org.mockito.Mockito.*;
import org.junit.After;
import org.junit.Before;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import com.kiri.mocksample.beans.UserInfo;
@ExtendWith(MockitoExtension.class) // JUnit5でMockito使うには必要
public class DisplayUserInfoTest {
// モック化するクラスのインスタンスを生成します。
@Spy
private UserInfo mockInfo;
private AutoCloseable closeable;
// モックを注入するクラスのインスタンスを生成します。
@InjectMocks
private DisplayUserInfo target;
// this(mockInfo)を初期化します。
@Before
void initService() {
closeable = MockitoAnnotations.openMocks(this);
// MockitoAnnotations.initMocks(this);
}
@After
void closeService() throws Exception {
closeable.close();
}
// ここがテストの中身
@Test
public void testMock() {
// テスト対象のクラスを実行します。
target.getUserInfoString("000001");
verify(mockInfo).voidFunction("000001");
}
}
モックは@Spyとし、getUserInfoString後にverifyメソッドを呼び出しています。
verify(mockInfo).voidFunction("000001");
の処理によりmockInfoのvoidFunction("000001")が1回呼び出されたかを検証できます。
複数の呼び出しが想定される場合は
verify(mockInfo, times(2)).voidFunction("000001");
とすることで、2回呼び出されたかを検証できます。
なお、呼び出しが1度も行われなかった場合や、指定回数と一致しなかった場合はテストがエラーとなります。
doNothingによる処理の無効化
@Spyでインスタンスを指定した場合、whenやdoReturnでのモック化できないvoidメソッドはテスト時に実行される。
実行されてもテストには影響しないかもしれないが、それ故に意図的に無効してしまいたい場合がある。
そのような場合はdoNothingでvoideメソッドを指定します。
具体的には以下の様にtestMock関数を編集します。
@Test
public void testMock() {
doNothing().when(mockInfo).voidFunction(anyString());
when(mockInfo.getName("000001")).thenReturn("鈴木一郎");
when(mockInfo.getGender(anyString())).thenReturn("男");
doReturn("42").when(mockInfo).getOld("000001");
// テスト対象のクラスを実行します。
String result = target.getUserInfoString("000001");
// 戻り値を確認する。
assertEquals("鈴木一郎(男) 42歳", result);
}
上記を実行しても、コンソールに「Hello, World!」の出力が行われなくなり、無効化されたことが確認できると思います。
利用ケース4(呼び出された引数の確認)
モック化したメソッドに対して、期待通りの引数が指定されたかを確認したい場合。
テスト対象のクラスを作成
利用ケース1で作成したクラスを使い回します。
クラスの編集
利用ケース1で作成したクラスを使い回します。
テストケースの作成
利用ケース1で作成したDisplayUserInfoTestクラスを、以下のように編集します。
package com.kiri.mocksample.case1;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import org.junit.After;
import org.junit.Before;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import com.kiri.mocksample.beans.UserInfo;
@ExtendWith(MockitoExtension.class) // JUnit5でMockito使うには必要
public class DisplayUserInfoTest {
// モック化するクラスのインスタンスを生成します。
@Spy
private UserInfo mockInfo;
private AutoCloseable closeable;
// モックを注入するクラスのインスタンスを生成します。
@InjectMocks
private DisplayUserInfo target;
// this(mockInfo)を初期化します。
@Before
void initService() {
closeable = MockitoAnnotations.openMocks(this);
}
@After
void closeService() throws Exception {
closeable.close();
}
// ここがテストの中身
@Test
public void testMock() {
ArgumentCaptor<String> capUserId = ArgumentCaptor.forClass(String.class);
when(mockInfo.getName("000001")).thenReturn("鈴木一郎");
when(mockInfo.getGender("000001")).thenReturn("男");
doReturn("42").when(mockInfo).getOld(capUserId.capture());
// テスト対象のクラスを実行します。
target.getUserInfoString("000001");
// 戻り値を確認する。
assertEquals("000001", capUserId.getValue());
}
}
メソッドの先頭で引数をストックする変数宣言を追加しています。
ArgumentCaptor<String> capUserId = ArgumentCaptor.forClass(String.class);
また、getOldの引数がcapUserId.capture()となっています。
これにより、テスト実行時、getOldメソッドに渡された引数をストックすることができます。
ストックされた変数情報は、
assertEquals("000001", capUserId.getValue());
のように、getValueメソッドにより取得できます(ストックが1つの場合)
複数の変数をストックする場合
以下のコードのように、getUserInfoString内で複数回getOldが呼び出された場合(ちょっと無理やりですが)
package com.kiri.mocksample.case1;
import com.kiri.mocksample.beans.UserInfo;
public class DisplayUserInfo {
private UserInfo info = new UserInfo();
public String getUserInfoString(String userId) {
String name = info.getName(userId);
String gender = info.getGender(userId);
String old = info.getOld(userId);
String old2 = info.getOld("000002");
String old3 = info.getOld("000003");
return name + "(" + gender + ") " + old + "歳";
}
}
以下のコードのようにgetAllValuesで全ての引数の値を確認できます。
// ここがテストの中身
@Test
public void testMock() {
ArgumentCaptor<String> capUserId = ArgumentCaptor.forClass(String.class);
when(mockInfo.getName("000001")).thenReturn("鈴木一郎");
when(mockInfo.getGender("000001")).thenReturn("男");
doReturn("42").when(mockInfo).getOld(capUserId.capture());
// テスト対象のクラスを実行します。
target.getUserInfoString("000001");
// 戻り値を確認する。
assertEquals("000001", capUserId.getAllValues().get(0));
assertEquals("000002", capUserId.getAllValues().get(1));
assertEquals("000003", capUserId.getAllValues().get(2));
}
void関数の引数を確認したい場合
verifyメソッドの引数にArgumentCaptorの変数を指定することで同様に引数の確認が行えます
// 例
verify(mockInfo, times(3)).voidFunction(capUserId.capture());
最後に
今回はMockitoライブラリをJUnitのテストで利用することで、内部で呼び出しているメソッドをモック化し戻り値を変更したり、無効化を行えることを確認しました。
通常出力しないエラーや頻度が稀なエラーなどのエラー処理テスト等で利用できるかと思いましたが、テスト要件などに合わせて利用すればよいかなと思いました。