mockito入門2 基本的な形

2019年5月16日

mockitoについての全投稿は/tag/mockitoにあるので参照されたい

mockito入門1 概要の続きである。

前述したようにmockitoはJUnitとの組み合わせが必須ではないのだが、簡単のためにこれ以降ではJUnitでの使用のみを対象とする。また、Hamcrestを全面的に使用することにする。Hamcrestについては、Hamcrestチュートリアルを参照のこと。

前回の例では、JUnitのassertTrue()などを使用していたが、今回からは使用しない。HamcrestのassertThat()を使うことにする。

必要なライブラリ

使用するライブラリは、JUnit4.19、Mockito2.19.0、Hamcrest1.3とすると、gradleでの依存は以下の記述になる。

dependencies {    
  testCompile group: 'org.mockito', name: 'mockito-core', version: '2.19.0'
  testCompile(group: 'junit', name: 'junit', version: '4.12') {
    exclude module:'hamcrest-core'
  }
  testCompile group: 'org.hamcrest', name: 'hamcrest-all', version: '1.3'
}

ユニットテストのパターン

JUnitによるユニットテストは以下のような骨格になる。

import static org.hamcrest.Matchers.*;
import static org.mockito.Mockito.*;
import static org.mockito.hamcrest.MockitoHamcrest.*;
import org.junit.*;
import org.mockito.*;

public class TestZero {

  @Mock Something something;
  @Mock Anything anything;

  @Before
  public void before() {
    MockitoAnnotations.initMocks(this);
  }

  @Test
  public void test1() {
  }

  @Test
  public void test2() {

  }
}
  • 「org.hamcrest.Matchers.」と「org.mockito.Mockito.」「org.mockito.hamcrest.MockitoHamcrest.*」をstaticインポートすることにより、これらのメソッドについてはメソッド名だけ書けば良いことになる。
  • 各テストの前段階として、この場合にはbeforeメソッドが呼ばれるが、そこでMockitoAnnotations.initMocks(this)とすることにより、(この場合には)新たなモックが生成され、@Mockで示された変数に代入される。

今後の説明では、このパターンを前提とするため、いきなりwhen()、spy()といったメソッドが登場するので注意すること。

※ネットに見られるサンプルには、わざわざMockito.when()と呼び出しているものもあるのだが、もちろんそうしたければそうしてもよい。

モックの動作指定とマッチャー

モックはそのままでは何もしてくれない。先の例では、モックの動作指定を全くしなければ、login()の引数として何を入れてもfalseを返すことになる。この例の場合では、特定のユーザ名・パスワードが入力されたらtrueを返したい。

例えば”foo”, “bar”のペアが入力された場合はtrueを返すにはこうする。

doReturn(true).when(mock).login("foo", "bar");

しかしこれでは、引数として与えられるすべての場合を記述しなくてはならない。これでは面倒なので、ワイルドカードともいうべき、特定のパターンであれば条件が成立したとみなすような書き方ができる。

例えば、名前は”foo”でパスワードは何でも良いものとすると、

doReturn(true).when(mock).login(eq("foo"), anyString());

これをマッチャーという。注意が必要なこととしては、引数の一つでもマッチャーを使用した場合には、全引数がマッチャーでなければならないこと。だから以下のようには書けない。

doReturn(true).when(mock).login("foo", anyString());

eq()という「指定された値に等しい」ことを意味するマッチャーを使わなければいけない。

当然だが、マッチャーを自作コードにして、パターンを指定することもできる。最も簡単な書き方としてはJava 8のラムダ式を使うものだ。例えば、以下は名前が”foo”で始まる場合を示す。

doReturn(true).when(mock).login(argThat(s->s.startsWith("foo")), anyString());

以下はHamcrestの大文字小文字を無視するマッチャーを使った例だ

// equalToIgnoringCase()はHamcrestのマッチャー
doReturn(true).when(mock).login(argThat(equalToIgnoringCase("foo")), anyString());

※現時点ではわからないかもしれないが、上の二つの例のargThatは同じ名前だが別のメソッドである。前者は、Mockitoにビルトインされたメソッドだが、後者は、Hamcrestのマッチャーを呼び出すための橋渡しだ。

想像できる通り、マッチャーの数は多い。これまでに登場したeq(), anyString(), argThat()の他にも様々なものがある。

Mockitoに用意されているマッチャー

Mockitoにビルトインされたマッチャーについては、Class ArgumentMatchersを参照のこと。

Hamcrestのマッチャー

HamcrestのマッチャーについてはHamcrestチュートリアルを参照のこと。このマッチャーを橋渡しとなるMockitoのメソッドargThat()等を通じて使用することができる。

Mockito用とHamcrest用での同名のメソッド

ややこしいことにMockito用とHamcrest用で同名のメソッドがある。例えば、以下を書いたとすると。

doReturn(true).when(mock).login(argThat(s->s.startsWith("foo")), anyString());
doReturn(true).when(mock).login(argThat(equalToIgnoringCase("foo")), anyString());

前者のargThatは、org.mockito.ArgumentMatchers#argThatであり、後者はorg.mockito.hamcrest.MockitoHamcrest.argThatになることに注意。当然のながら引数は異なる。前者はMockitoで定義されているArgumentMatcherインターフェース型の引数であり、後者はHamcrestで定義されるMatcherインターフェースになる。そして、前者のインターフェースには複数のメソッドが定義されているため、ラムダ式として書くことはできず、後者のみがラムダ式として記述でrきう。

モックの呼び出し検証

モック化するときにマッチャーを使うことと同様、検証のときにも同じマッチャーが使用できる。
以下は大文字小文字問わず、fooという名前のログインが二回あったことを検証する例だ。

    mock.login("FOO",  "AAA");
    mock.login("Foo",  "BBB");
    verify(mock, times(2)).login(argThat(equalToIgnoringCase("foo")), anyString());  

以上で、以下が可能になった。

  • 引数の内容に応じて返り値を設定する。
  • 指定引数で何度呼び出されたかを検証する。

モック呼び出しで副作用を作りたい場合

良いプログラミングスタイルでは無いが、ときにはこういう必要性もある。以下のインターフェースからモックを作ることにする。

  public interface Something {
    public void addSomeElements(List<String>list);
  }

テストコードがリストを渡したら、モックの中でそのリストの操作をしてもらいたい。要素を追加する等だ。これにはAnswerという機能を使うほかなさそうだ。

import static org.hamcrest.Matchers.*;
import static org.mockito.Mockito.*;
import static org.mockito.hamcrest.MockitoHamcrest.*;

import java.util.*;

import org.junit.*;
import org.mockito.*;
import org.mockito.stubbing.*;

public class Sample {
  @Mock Something mock;

  @Before
  public void before() {
    MockitoAnnotations.initMocks(this);
  }

  @Test
  public void test() {
    Answer<Void>ans = inv-> { 
      @SuppressWarnings("unchecked")
      List<String>list = (List<String>)inv.getArgument(0);
      list.add("foo");
      return null; 
    };
    doAnswer(ans).when(mock).addSomeElements(any());

    List<String>list = new ArrayList<>();
    mock.addSomeElements(list);
    System.out.println("" + list);
  }

  public interface Something {
    public void addSomeElements(List<String>list);
  }
}

Answerに与えられる引数から、メソッドのすべての引数を取得することができる。この場合には、0番目の引数を取得し、それをリストとして要素を追加する。Answerの返り値がメソッドの返り値になるのだが、上記の場合はvoidなので、この場合はVoid型を指定する。