Mockitoとは
- JUnitと同じくテストを自動化するためのライブラリ
- モックを使用するテストを「モックテスト」と呼ぶ。
- モック 伊藤
Mockitoのメリット
サイトを浅り読んだ限りでは以下のことが挙げられるそうだ。
- テストのためにテスト対象クラスを書き換えるという行為を避けることができる。
- 再現の難しい(乱数が絡んだり)テストにて、固定値化することで、期待値と
一致するかどうかを確かめやすくする。
事前準備
- pom.xmlにdependencyを追加する。
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>1.10.19</version>
</dependency>
- MockのクラスとMock注入先クラスを用意する。
// モッククラス
public class DBConnecter {
public String getConnecter() {
return "_Connection_";
}
}
public class SampleDAO {
// モック注入先クラス
private DBConnecter connecter;
public select() {
System.out.println("接続情報「" + connecter.getConnecter() + "」を使用してSELECTします。");
}
}
モックテストクラス
①モック注入をしない場合
- 実行結果はsampleDAO#connecterは当然nullなのでgetConnecter()呼出時に
NullPointer例外になる。
public class SampleDAOTest
{
DBConnecter connecter = new DBConnecter();
SampleDAO sampleDAO = new SampleDAO();
@Before
public void init() {
}
@Test
public void testcase001() {
sampleDAO.select();
assertEquals(true, true);
}
}
②-1モック注入をする。(@Mock, @InjectMock, initMock())
-
@Mockをモッククラスに付与
DBConnecter connecter = mock(DBConnecter.class);でも同様の挙動を見せる。 - @InjectMockをモック注入先クラスに付与
- MockitoAnnotations.initMocks(this);
でアノテーションを有効化する。これがないと①と同様にNullPointer例外のままになる。
public class SampleDAOTest
{
@Mock
DBConnecter connecter;
@InjectMocks
SampleDAO sampleDAO = new SampleDAO();
@Before
public void init() {
MockitoAnnotations.initMocks(this);
}
@Test
public void testcase001() {:thinking:
sampleDAO.select();
assertEquals(true, true);
}
}
【出力結果】
接続情報「null」を使用してSELECTします。
あれ?connecterのインスタンスは作成されたのに、「null」なのはなぜに?
②-2 デバックで覗いてみた。
- DBConnecter#getConnecter()にブレークポイントを置いてみた。
⇒ ここで停止できませんというエラーが発生した。 - sampleDAO にカーソルを合わせてSampleDAO#connecterの型を確認してみた。
⇒ 謎な型 : DBConnecter#EnhancerByMockito~~~ - getConnection()の定義にSystem.out.println("XXX")を置いてみた。⇒ 呼ばれない。
上記3点から@Mockによるインスタンスはnewのインスタンスと明らかに異なる。
そのため「モック」(実物に似た模型)と呼ばれているのだなぁ
(厳密には空とは少し違うかもだが感覚をつかむ分にはいいでしょう。)
③-1 - モックにテスト用戻り値を設定する。(引数なし) when().thenReturn(), doReturn().when
- when(mockInstance#mockMethod()).thenReturn("want to set value");
特定のモックインスタンスが呼ばれたとき(when)、指定した値を返却(return)する。
【出力結果】
接続情報「override result by when()」を使用してSELECTします。
when(connecter.getConnecter()).thenReturn("override result by when()");
sampleDAO.select();
assertEquals(true, true);
③-2 - モックにテスト用戻り値を設定する。(引数が固定値) when().thenReturn()
- mockMethod()が引数ありの場合、引数に応じて設定値を変えられる。
// モッククラス
public class DBConnecter {
public String getConnecter(int i, boolean b) {
return "_Connection_";
}
}
public class SampleDAO {
// モック注入先クラス
private DBConnecter connecter;
public select() {
System.out.println("接続情報「" + connecter.getConnecter(1, true) + "」を使用してSELECTします。");
System.out.println("接続情報「" + connecter.getConnecter(2, false) + "」を使用してSELECTします。");
System.out.println("接続情報「" + connecter.getConnecter(-1, false) + "」を使用してSELECTします。");
}
}
when(connecter.getConnecter(1, true)).thenReturn("getConnection() args 1, true");
when(connecter.getConnecter(2, false)).thenReturn("getConnection() args 2, false");
sampleDAO.select();
assertEquals(true, true);
【出力結果】
接続情報「getConnection() args 1, true」を使用してSELECTします。
接続情報「getConnection() args 2, false」を使用してSELECTします。
接続情報「null」を使用してSELECTします。
③-3 - モックにテスト用戻り値を設定する。(引数が任意値) when().thenReturn()
- ③-2の場合、引数は100通りあるけど戻り値は1通りなんだけどという場合when()を100行書かないといけない。
- そういう引数は何でもいいんだけどという場合はMockito.Matchers.anyXXX()を使用する。
when(connecter.getConnecter(anyInt(), anyBoolean())).thenReturn("getConnection() args any, any");
sampleDAO.select();
assertEquals(true, true);
【出力結果】
接続情報「getConnection() args any, any」を使用してSELECTします。
接続情報「getConnection() args any, any」を使用してSELECTします。
接続情報「getConnection() args any, any」を使用してSELECTします。
④-1 モックに例外を設定する。 doThrow().when()
- 戻り値がvoid型の場合、when().thenReturn()は使えない。
- ③では戻り値を設定していたが、④-1では例外を設定することができる。
- 本来の使い方としてはthrowsで投げられる可能性がある例外を設定すべき
(doThrowにはNullPointerExceptionを設定すべきということ。)
public class Sample1{
public void voidMethod() throws NullPointerException {
throw new NullPointerException();
}
}
doThrow(new IllegalArgumentException()).when(sample1).voidMethod();
try {
sample1.voidMethod();
} catch (NullPointerException e) {
System.out.println("NullPointerExceptionが発生しました。");
} catch (IllegalArgumentException e) {
System.out.println("IllegalArgumentExceptionが発生しました。");
}
}
【出力結果】
IllegalArgumentExceptionが発生しました。
⑤ モックの一部のみテスト用戻り値を設定する。@Spy
public class Sample1{
public String method1() {
return "called method1";
}
public String method2() {
return "called method2";
}
}
public class SampleDAOTest
{
@Spy
Sample1 sample1 = new Sample1();
@Before
public void init() {
}
@Test
public void testcase001() {
when(sample1.method1()).thenReturn("when called method1");
System.out.println(sample1.method1());
System.out.println(sample1.method2());
【出力結果】
when called method1
called method2
⑥ Verifyを用いたテスト
⑥-1 メソッドが指定回数呼ばれたか判定 times
- verify(spy or mock, times(want)).method(args)でtime(want)には
メソッドの実行回数の期待値を設定する。 - 実際の回数が期待値が異なる場合例外が発生しテストケースが失敗する。
- argsにはwhen()と同様にanyXXX()による指定ができる。
public class Sample1{
public String method1(int i) {
return "i";
}
}
sample1.method1(10);
sample1.method1(20);
sample1.method1(20);
verify(sample1,times(2)).method1(20); // 1回目
verify(sample1,times(2)).method1(anyInt()); // 2回目
verify(sample1,times(3)).method1(20); // 3回目
【出力結果】
3回目のverify()で例外が発生。
TooLittleActualInvocations() sample1.method1(20);
Wanted 3 times: But was 2 times
⑥-2 メソッドが指定回数呼ばれたか判定 atLeastOnce, atLeast, atMost, never
- verifyの引数にはtimes()以外にも渡すことができる。
- 実際の回数が期待値が異なる場合例外が発生しテストケースが失敗する。
verify(sample1,atLeastOnce()).method1(0); // 最後に呼ばれたメソッドと引数が一致で正常
verify(sample1, atLeast(2)).method1(20); // 2回「以上」で正常
verify(sample1, atMost(2)).method1(20); // 2回「以下」で正常
verify(sample1,never()).method1(20); // 0回で正常 (times(0))と等価
【出力結果】
回数 | 例外メッセージ |
---|---|
atLeastOnce | Argument(s) are different! |
atLeast | TooLittleActualInvocations |
atMost | MockitoAssersionError |
never | NeverWantedButInvoked |
⑦ private変数へのInjectをする。 Whitebox.setInternalState()
- やることは@MockInjects,@mock,@spyと等価。(厳密には違うかもだが)
- 第二引数が文字列になっておりミスタイプが起こる可能性があるため
setInternalState()は使わず、@MockInjects,@mock,@spyを使うのがよい。
public class SampleDAOTest {
Sample1 sample1 = new Sample1();
Sample2 sample2 = new Sample2();
@Before
public void init() {
Whitebox.setInternalState(sample2, "sample1", sample1);
}
@Test
public void testcase001() {
System.out.println("2倍した結果は「" + sample2.twice(10) + "」です。");
}
}
完走した感想
- 初めてこのサイトで記事を書いたが、やはりソースコードのフォーマッタが簡単にかけられるのが良い。
- 無計画に記事を書いたので関連すべき情報がとっ散らかってしまった。
(@Mockと@Spyとかが近くに掛かれていなかったり)
残課題
・公式のガイドを一通り目を通したい。
・doXXX()のその他の使いかた
.when().thenReturn()とdoXXX().when()の違い。
・PowerMockとは?