mockito入門1 概要

2018年6月27日

モックフレームワークは何がうれしい

通常、ユニットテスト等を行う場合には、テスト対象のクラスから本番で呼び出されるメソッドなりクラスなりをダミーのものに差し替えて行う。これらのダミーをモックとかスタブとか呼ぶ。
おおざっぱに言うと、モックは単にそこに存在するだけ(ただし返り値が必要であればそれを返す)、スタブはその中にビジネスロジックが含まれるものを指すらしい。

これらのモックやスタブは自ら記述してももちろん構わない。例えば、

public interface Login {
  /** ユーザ名・パスワードを与え、ログイン成功したらtrueを返す */
  boolean login(String user, String password);
}

というインターフェースに対して、以下のような「空」の実装を作るわけだ。

public class LoginImpl implements Login {
  /** ユーザ名・パスワードを与え、ログイン成功したらtrueを返す */
  public boolean login(String user, String password) {
    return true;
  }
}

このような実装クラスを記述してテストに使う。
単純なテストではこれで構わないのだが、例えば以下のようなテストを行いたい場合がある。

  • 複数のユーザ・パスワードの組について、ログイン成功する場合と失敗する場合をテストしたい。
  • それらの組について、(テスト対象ユニットから)きちんと呼び出されているかを確かめたい。
  • (この場合にはそういう要求はないだろうが)呼び出し順序が正しいかを確かめたい。

などなどがある。これを自ら記述したモッククラスで行うには、例えば以下のように記述しなければいけない(呼び出し順序とユーザ・パスワードの組により成功・失敗する例)。

public class LoginImpl implements Login {
  int callIndex = 0;

  /** 特定のユーザ名・パスワードについて成功するものとする */
  public boolean login(String user, String password) {
    switch (callIndex++) {
    case 0: return user.equals("user1") && password.equals("password1");
    case 1: return user.equals("user2") && password.equals("password2");
    default: return false;
    }
  }
}

わざわざこのような実装を記述しなくとも、様々なテスト条件を簡単に記述できるというのがモックフレームワークの「うれしい」ところである。

mockitoは何がうれしい

Java用のモックフレームワークは、他にEasyMock, jMock等があるが、mockitoは後発だけあって、先発のそれらの使い勝手がかなり改善されているとのこと。

特にJava5以降のstatic importを多用した簡単構文のおかげでモックの定義が記述しやすくなっている模様。

mockitoの基本的な使い方

もちろん、モックフレームワークはテストに使うのがほぼ100%と思われるが、特にJUnit等のテストフレームワークが必須であるわけではない。独立して使うこともできる。

もっとも単純な場合は以下。

import org.mockito.*;
public class TestZero {
  public interface Login {
    boolean login(String user, String password);
  }  
  public static void main(String[]args) {
    Login mock = Mockito.mock(Login.class);
    System.out.println(mock.login("foo", "bar"));
  }
}

インターフェースLoginを定義し、Mockito.mockでそのインスタンスを取得する。loginメソッドを適当なパラメータで呼び出し、結果を出力する。この場合の結果はfalseである。

mockitoのマジック

上述のコードには一つおかしなところがある。それは、Loginインターフェースの実装がどこにも記述されていないこと。通常であれば、

class LoginImpl implements Login {
  public boolean login(String user, String password) {
    return false;
  }
}

というLoginインターフェースを実装したクラスがどこかに存在しなければならないはず。しかしそれが必要ないのである。「Mockito.mock(Login.class)」と呼び出すだけで、勝手に実装クラスが生成されて、そのインスタンスが返されるかのようだ。

このような機能がJavaには存在する。例えば、Java-APIに含まれるjava.lang.reflect.Proxyがある。このクラスを使うと、任意のインターフェースの実装をその場で生成することができる。

しかし、mockitoの仕組みはもっと高度のもののようだ。モック対象はインターフェースでなくとも通常のクラスでも構わないのだが、詳しい仕組みは未調査である。

mockitoで作成されたモックの動作

上述のように、mockitoは指定されたインターフェースやクラスから、その実体(モック)を作り出す。

ここで、そのモックに「何も定義しない場合はどうなるか」だが、つまり先に述べた例では、login(Srting user, String password)が呼び出されたときに「どうするか」を何も指定していないのだが。この場合にはどうなるか?

mockitoでは「何もしない」、つまり例外は起こらないのでテストケースの場合であれば、呼び出し自体は成功である。

ただし、返り値としては、それらのデフォルト値としてnull, false, 0等などが返される。したがって、上記のlogin呼び出しは結局のところ必ず失敗値(false)が返る。

テストユニットに記述する例

テストユニットとして記述する場合も何ら変わりはない。JUnitの場合は例えば以下のように記述するかもしれない(このテストケースは必ず失敗である)。

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

public class TestZero {
  public interface Login {
    boolean login(String user, String password);
  }  
  @Test
  public void test() {
    Login mock = Mockito.mock(Login.class);
    Assert.assertTrue(mock.login("foo", "bar"));
  }
}

※Assert.assertTrueはJUnitのメソッド

モックの動作の指定と検証

次はmockitoで作成したモックの動作の指定と検証を行ってみる。

  @Test
  public void test() {
    Login mock = Mockito.mock(Login.class);

    // "foo", "bar"という組であればtrueを返す指定
    Mockito.doReturn(true).when(mock).login("foo", "bar");

    // JUnitのメソッドでチェック("bar", "foo"の場合は失敗)
    Assert.assertTrue(mock.login("foo", "bar"));
    Assert.assertFalse(mock.login("bar", "foo"));

    // 呼び出しがあったことを確認。("foo", "bar"), ("bar", "foo")は一度ずつ
    // ("foo", "foo")は一度も呼び出されていない。
    Mockito.verify(mock, Mockito.times(1)).login("foo", "bar");
    Mockito.verify(mock, Mockito.times(1)).login("bar", "foo");
    Mockito.verify(mock, Mockito.never()).login("foo", "foo");
  }

テストユニットは通常、Assert.~、Mockito.~だらけになってしまうので、これらを省略するために、import static構文を使う。また、times(1)は省略可能である。

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

import org.junit.*;

public class TestZero {
  public interface Login {
    boolean login(String user, String password);
  }  
  @Test
  public void test() {
    Login mock = mock(Login.class);

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

    assertTrue(mock.login("foo", "bar"));
    assertFalse(mock.login("bar", "foo"));

    verify(mock).login("foo", "bar");
    verify(mock).login("bar", "foo");
    verify(mock, never()).login("foo", "foo");
  }
}

アノテーションの利用

アノテーションを利用することにより、このユニットテストで利用するモックをあらかじめ作成しておくことができる。上記の場合には、Login mockの部分だが、以下のようにできる。

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

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

public class TestZero {

  public interface Login {
    boolean login(String user, String password);
  }  

  /** @Mockアノテーションにより、モックであることを示す */
  @Mock Login mock;

  @Before
  public void before() {
    /** このクラス内にある@Mockによるモックをすべて作成する */
    MockitoAnnotations.initMocks(this);
  }

  @Test
  public void test() {
    // ここでのモックの生成は不要になる。

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

    assertTrue(mock.login("foo", "bar"));
    assertFalse(mock.login("bar", "foo"));

    verify(mock).login("foo", "bar");
    verify(mock).login("bar", "foo");
    verify(mock, never()).login("foo", "foo");
  }
}

この例ではテストは一つしかないのだが、複数のテストが存在する場合には、それぞれの前段階としてbefore()メソッドが呼び出され、モックが新たに生成される。この機能はJUnitの機能である。

doReturn/whenかwhen/thenReturnか?

Mockitoでは、なぜか同じことを二種類のやり方で書くことができる。先の例の

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

は以下のように書いても同じことになる。

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

二種類ある理由はわからないのだが、ここでは前者のやり方に統一する。理由は以下を参照して欲しい。

Mockito~doReturn/whenとwhen/thenReturnの違い