mockito概要
モックフレームワークは何がうれしい
通常、ユニットテスト等を行う場合には、テスト対象のクラスから本番で呼び出されるメソッドなりクラスなりをダミーのものに差し替えて行う。これらのダミーをモックとかスタブとか呼ぶ。 おおざっぱに言うと、モックは単にそこに存在するだけ(ただし返り値が必要であればそれを返す)、スタブはその中にビジネスロジックが含まれるものを指すらしい。
これらのモックやスタブは自ら記述してももちろん構わない。例えば、
public interface Login { boolean login(String user, String password); }
というインターフェースに対して
public class LoginImpl implements Login { 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-APIに含まれるjava.lang.reflect.Proxyによるものと思われる。このクラスは、指定された任意のインターフェースを実装するクラス無しで、そのインスタンスを生成することができる。
そのメソッド呼び出しの処理は、生成時に指定されるInvocationHandlerにて行われる。mockitoもこの機能を使用しているものと思われる。
mockitoで作成されたプロキシの動作
上述のように、mockitoは(おそらく)Javaのプロキシ機能を使用して、インターフェース定義だけからそのインスタンスを作成する。そのメソッド呼び出しの処理はInvocationHandlerを介して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.when(mock.login("foo", "bar")).thenReturn(true); // 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); when(mock.login("foo", "bar")).thenReturn(true); 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"); } }