テスト工程で使用する__"mock"__についてまとめてみました。
mockとは?
mockとは、一言でいうと__テストに必要な部品の値を擬似的に設定するもの__です。
例えば、クラスAの単体テストのコードを書く際。
「あれ?今作ってるテストに必要なクラスBが未完成だからテストコードが作れないのでは?」
「あれ?今作ってるテストに必要なクラスBの処理が膨大すぎて変更が大変では?」
と、壁にぶち当たるときがあると思います。
テストを行うクラス以外の中身をわざわざ変更するのも手間だし、変更する先のクラスが複雑で膨大な処理を行っていたら、変更、保存、実行の数だけ工数が膨れ上がります。
そんな困った問題が起きたときに使うのが__"mock"__です。
テストを行うクラスが他クラスのメソッドの戻り値を使用している時、mockを使うことで他クラス自体をいじらずに、その戻り値のみを自由に設定することができます。つまりテストに必要な部品を補うことができるのです。
#実際に何をどうやって使うの?
実際どのように使うのか見てみたほうが早いと思うので、例えばjavaではどのような感じにmockを作成するのか、例を挙げて説明したいと思います。
今回使用フレームワーク&ライブラリは下記です。
- Spring Boot
- JUnit
- mockito
#環境設定をしよう
今回は、Spring Bootを使用するのでeclipseのメニューの「ファイル>新規>その他」をクリックしましょう。
Spring BootのSpringスターター・プロジェクトを選択後、次へをクリックしましょう。
出現したウィンドウの入力欄を
名前:demo
型:Maven
と入力し、他の入力欄は触らず次へをクリックしましょう。
テンプレートエンジンは、Thymeleefを選択。
WebはWebを選択し、完了をクリックしましょう。
これでSpring Bootの設定は完了しました。
#実際にテストするクラスを作ろう
今回は「あるシステムのおみくじ機能の単体テスト」という体で進行していきます。
おみくじ機能には、実際に占いを行うFortuneChoiceクラスとユーザー情報を渡す役割のUserInfoクラスの2つがあり、FortuneChoiceクラスをテストするFortuneChoiceTestクラスがtest用のパッケージに入っています。
しかるべきパッケージにjavaファイルを作成しましょう。
以下の二つのクラスが占いをしてくれるクラスになります。
import java.util.Random;
import com.example.beans.UserInfo;
public class FortuneChoice {
public static final String DAIKYOU = "大凶";
public static final String KYOU = "凶";
public static final String KITI = "吉";
public static final String SUEKITI = "末吉";
public static final String TYUKITI = "中吉";
public static final String DAIKITI = "大吉";
private UserInfo info = new UserInfo();
public String choice () {
// info.~と書いてあるところをmock化したい。
String name = info.getName(1);
String lank = info.getLank(1);
String sex = info.getSex(1);
Random rnd = new Random();
int choice = rnd.nextInt(100)+1;
String fortune = "";
if(choice <= 10) {
fortune = DAIKYOU;
} else if (choice > 10 && choice <= 30) {
fortune = KYOU;
} else if (choice > 30 && choice <= 50) {
fortune = KITI;
} else if (choice > 50 && choice <= 70) {
fortune = SUEKITI;
} else if (choice > 70 && choice <= 90) {
fortune = TYUKITI;
} else if (choice > 90 && choice <= 100) {
fortune = DAIKITI;
}
if(isBlank(name)) {
name = "あなた";
} else {
name = name + "さん";
}
if(isBlank(sex)) {
sex = "人間";
}
if(isBlank(lank)) {
lank = "一般会員";
}
return lank + "の," + "性別は" + sex + "の," + name + "の運勢は、," + fortune + "です。";
}
public static boolean isBlank (String str) {
if(str.isEmpty()) {
return true;
}
if(str.indexOf(" ") > -1) {
return true;
} else if(str.indexOf(" ") > -1) {
return true;
}
return false;
}
}
public class UserInfo {
public String getName(Integer id) {
String name = "";
if(id == 1) {
name = "福田";
} else if(id == 2) {
name = "Alex";
} else {
name = "mock";
}
return name;
}
public String getSex(Integer sexId) {
String sex = "";
if(sexId == 1) {
sex = "男";
} else if(sexId == 2) {
sex = "女";
} else {
sex = "人間";
}
return sex;
}
public String getLank(Integer lankId) {
String lank = "";
if(lankId == 1) {
lank = "一般会員";
} else if(lankId == 2) {
lank = "プレミアム会員";
}
return lank;
}
}
FortuneChoiceクラスは、
与えられたユーザーIDやランクIDを元に情報を持ってきて( *1)、占い結果とともに返す仕組みになっています。
テスト対象は実際に占いを行うFortuneChoiceクラスです。
FortuneChoiceクラスが使っているユーザー情報を渡す役割のUserInfoクラスも記述していますが、
まだ未完成とします。
こんなときはFortuneChoiceクラスのテストクラスに__mockito__というライブラリを使いmockを作成し、テストを行います。
mockの作成、テストの実行
いよいよテストクラスの作成を行います。
その前に、テストに必要な__JUnit__というフレームワークをプロジェクトに適応させましょう!
demoプロジェクトを右クリックし、プロパティーをクリックしましょう。
ライブラリーの追加をクリックしましょう。
JUnitを選択し、次へをクリックしましょう。
JUnit4を選択し、完了をクリックしましょう。
適応して閉じるをクリックしたらJUnit適応完了です。
そしていよいよmockを使ったテストクラスの生成です。
下記はmockを使ったテストコードです。
import static org.junit.Assert.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.test.context.junit4.SpringRunner;
import com.example.beans.UserInfo;
@RunWith(SpringRunner.class)
public class FortuneChoiceTest {
// モック化するクラスのインスタンスを生成します。
@Mock
private UserInfo mockInfo = new UserInfo();
// モックを注入するクラスのインスタンスを生成します。
@InjectMocks
private FortuneChoice fc = new FortuneChoice();
// this(mockInfo)を初期化します。
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
}
//ここがテストの中身
@Test
public void testMock() {
// ここでモックを作成しています。
when(mockInfo.getLank((Integer)anyInt())).thenReturn("一般会員");
when(mockInfo.getSex((Integer)anyInt())).thenReturn("男");
when(mockInfo.getName((Integer)anyInt())).thenReturn("mock");
// テスト対象のクラスを実行します。
String result = fc.choice();
String[] resultArray = result.split(",");
String str1 = resultArray[0];
String str2 = resultArray[1];
String str3 = resultArray[2];
// 戻り値を確認する。
assertEquals("一般会員の", str1);
assertEquals("性別は男の", str2);
assertEquals("mockさんの運勢は、", str3);
}
}
急に@が出てきました。
これは__アノテーション__といって__機能の目印__のようなものです。
このテストクラスにはJUnit特有のアノテーションとmockito特有のアノテーションが存在しています。
今回扱うアノテーションの簡単な説明を記しておきます。
@Mock
mockito特有のアノテーション
ここではmock化したいクラスを取り扱います。
今回はmockInfoというインスタンス変数でインスタンスを生成しています。
@InjectMocks
mockito特有のアノテーション
ここではmock化したクラスに依存しているテスト対象のクラスを取り扱います。
今回はfcというインスタンス変数でインスタンスを宣言しています。
@Before
JUnit特有のアノテーション
ここでは@Testがついているメソッドが実行される前に実行してほしい処理を記述しておきます。今回は@Mockを使っているので、mockInfoを初期化する処理を記述してあります。
@Test
JUnit特有のアノテーション
ここでは、実行したいテストを記述します。
今回はmockを使用し、UserInfoの値を受け取ったFortuneChoiceクラスを実行して、その戻り値が想定どおりなのかを確認する処理が記述してあります。(ちなみに、通常のテストクラスならば、@Testがついたテストメソッドがずらーーーっと並びます。)
@TestのアノテーションがついているtestMockメソッドの処理に関して、さらに詳しく説明したいと思います。
まず
// ここでモックを作成しています。
when(mockInfo.getLank((Integer)anyInt())).thenReturn("一般会員");
when(mockInfo.getSex((Integer)anyInt())).thenReturn("男");
when(mockInfo.getName((Integer)anyInt())).thenReturn("ぴよぴよ");
mockを作成しているこの部分について説明します。
mockitoにはmock化したインスタンスのメソッドの戻り値を設定できる__whenメソッド__というものがあります。
// whenの中に戻り値を変更したいメソッドを入れる。
// when内のメソッドに引数を入れる場合はany~メソッドを使うとやりやすい
// どんな数字を入れても一般会員という文字列が返ってくる。
when(mockInfo.getLank((Integer)anyInt())).thenReturn("一般会員");
when()の中に戻り値を変更したいメソッドを入れ、thenReturn内に設定する値を入れます。
when()の中のメソッドにどんな値を入れても、一般会員という文字列が戻り値として返ってきます。
これでmockは作成完了です。
次に
// テスト対象のクラスのメソッドを実行します。
String result = fc.choice();
String[] resultArray = result.split(",");
String str1 = resultArray[0];
String str2 = resultArray[1];
String str3 = resultArray[2];
// 戻り値を確認する。
assertEquals("一般会員の", str1);
assertEquals("性別は男の", str2);
assertEquals("ぴよぴよさんの運勢は、", str3);
クラスのメソッドを実行して、戻り値を確認する処理の説明です。
ここでは、JUnitのメソッドである、assertEqualsというメソッドを使用しています。
assertEqualsは、引数にいれた二つの値が一致しているのかを判断するメソッドです。
// 一つ目の引数に予想される値を入れ、二つ目の引数に実際の値を入れる。
assertEquals("一般会員の", str1);
今回のテストメソッドはすべてのassertEquals内の値が一致していたら成功となります。
(JUnitには他にもテストで使用できるメソッドがありますが、説明は割愛します。)
この状態でテストメソッドを実行して
こんな感じになったら成功です。
ここで、
// getNameの戻り値の中身を姓名(スペースあり)にしてみましょう。
when(mockInfo.getLank((Integer)anyInt())).thenReturn("一般会員");
when(mockInfo.getSex((Integer)anyInt())).thenReturn("男");
when(mockInfo.getName((Integer)anyInt())).thenReturn("ぴよ ぴよ");
getNameの戻り値を姓名(スペースあり)にして、テストメソッドを実行してみましょう。
「ぴよ ぴよさんの運勢は、」となるはずが「あなたの運勢は、」となっているために失敗しています。
実は、FortuneChoiceクラスにバグがありました。
// この部分でtrueになっているため、あなたの、と表示される。
if(isBlank(name)) {
name = "あなた";
} else {
name = name + "さん";
}
// isBlankメソッドを見てみましょう。
public static boolean isBlank (String str) {
if(str.isEmpty()) {
return true;
}
// 空文字が一文字でも入っているためtrueになってしまっている。
if(str.indexOf(" ") > -1) {
return true;
} else if(str.indexOf(" ") > -1) {
return true;
}
return false;
}
isBlankメソッドの文字が入っているか判定する処理が間違っているため、姓と名の間のスペースが引っかかってしまい値が入っていない空白扱いになってしまい想定と違う結果が返っていました。バグがあるとわかったので、修正しましょう。
ということでテストがとても効率的になる__"mock"__の説明でした。
今回はmockitoを使っって説明しましたが、基本的な概念は他のライブラリーを使用しても同じなので、行き詰ったときなどにこの記事を見返していただけると幸いです。