メソッド呼び出しの引数をテストする

前回に引き続き、ユニットテストに利用するためのモックオブジェクトを簡単に作成することができる「EasyMock」について紹介する。

EasyMockの最も基本的な使い方は、対象のメソッドを特定の引数を与えて実行した場合に、正しい戻り値を返すようなモックを作成するというものである。しかしそれだけではなく、メソッドに渡された引数が想定通りであるかどうかをチェックするというような使い方もできる。例えば前回のサンプルとして作成したDataSourceクラスのモックは、get()メソッドが"testkey"という引数を受け取るように想定したものである。したがって、次のような"testkey"以外の引数を渡す呼び出しはエラーになる()

メソッド呼び出し時の引数が間違っているとテストは失敗する

String result = sample.findValue("wrongkey");

想定する引数は、もう少し柔軟性を持たせることもできるようになっている。例えば"testkey"限定ではなく、任意の文字列を渡せるようにしたい場合には、EasyMockクラスのanyObject()メソッドを利用して次のようにモックの振る舞いを定義すればよい。anyObject()は任意のオブジェクトを表すメソッドなので、Stringにキャストして使用している。

EasyMock.expect(mock.get((String)EasyMock.anyObject())).andReturn("resultValue");

EasyMockクラスには、anyObject()以外にも引数の型をチェックするために様々なメソッドが用意されている。主なものを以下に挙げる。

  • anyInt() - 任意のint値
  • anyShort() - 任意のshort値
  • anyByte() - 任意のbyte値
  • anyLong() - 任意のlong値
  • anyFloat() - 任意のfloat値
  • anyDouble() - 任意のdouble値
  • anyBoolean() - 任意のboolean値
  • notNull() - null以外の値
  • eq(x) - xと等しい値
  • gt(x) - xより大きい値
  • geq(x) - x以上の値
  • lt(x) - xより小さい値
  • geq(x) - x以下の値
  • isA(Class<T>) - 指定されたクラスの実装オブジェクト

この他に、正規表現を使って受け取る文字列を指定できるmatches()やfind()といったメソッドもある。対象が文字列であるならば、これらのメソッドを使えばより柔軟な引数チェックを行うことが可能だ。

メソッドの呼び出し順序を検証する

EasyMockでは、作成したモックに対するメソッド呼び出しについて、その呼び出し回数や呼び出し順序が適切かどうかを検証する機能も備えられている。この検証はEasyMockクラスのverify()メソッドを呼び出すことで実行できる。例えば次のコードでは、DataSourceのモックとしてget()メソッドの呼び出しに対する振る舞いを定義している。この場合、デフォルトではget()は1回だけ呼び出されることを想定したものとなる。したがって、get()が呼ばれていなかったり、2回以上呼ばれていたりすると、のような感じでテストは失敗する。

メソッドの呼び出し回数が想定の通りでない場合はテストが失敗する

呼び出し回数を複数回にしたり、メソッドの呼び出し順序を検証することもできる。これを試すために、DataSourceインタフェースを書き換えて、次のように3つのメソッドを持つようにしてみた。

public interface DataSource {
  public String get(String key);


  public void openSource();
  public void closeSource();
}

ここで、get()を呼ぶ場合には必ず最初にopenSource()メソッドを、最後にcloseSource()メソッドを呼ばなければならないことが決められていると仮定する。そのような呼び出しを行うメソッドとして、EasyMockSampleクラスに次のようなgetValues()メソッドを追加した。このメソッドは、指定された回数だけget()を実行してデータを取得し、その結果を返すというものである。

public List<String> getValues(int number) {
    List<String> values = new ArrayList<String>();
    this.dataSource.openSource();
    for(int i=0; i<number; i++) {
        this.dataSource.get("testkey");
    }
    this.dataSource.closeSource();
    return values;
}

このgetValues()メソッドが、正しい順序でDataSourceのメソッド呼び出しを行っていることを検証するには、次のようなテストを実行すればよい。

 @Test
 public void testFindValue() {
     // 動作モードを指定してモックを作成
    DataSource mock = EasyMock.createStrictMock(DataSource.class);

    // モックの振る舞いを記録
    mock.openSource();
    EasyMock.expectLastCall().times(1);
    EasyMock.expect(mock.get((String)EasyMock.anyObject())).andReturn("resultValue");
    EasyMock.expectLastCall().atLeastOnce();
    mock.closeSource();
    EasyMock.expectLastCall().times(1);
    EasyMock.replay(mock);

    // テストを実行
    EasyMockSample sample = new EasyMockSample();
    sample.setDataSource(mock);
    List<String> result = sample.getValues(3);


    // メソッド呼び出しを検証
    EasyMock.verify(mock);
 }

まず、モックの作成にはEasyMock.createStrictMock()メソッドを使う。EasyMockクラスでは、verify()メソッドによるチェックの厳密さを決める3つのモードが用意されている。これは、次のようにモック作成時に使うメソッドによって決定される。

  • EasyMock.createMock() - メソッド呼び出しの引数や、想定外のメソッド呼び出しが行われていないかはチェックするが、メソッドの呼び出し順序はチェックしない。
  • EasyMock.createStrictMock() - メソッド呼び出しの引数や、想定外のメソッド呼び出しが行われていないか、メソッドの呼び出しの順序は適切かを全てチェックする。
  • EasyMock.createNiceMock() - メソッド呼び出しの引数はチェックするが、メソッドの呼び出し順序はチェックせず、想定外のメソッド呼び出しが行われている場合でもテストは失敗にしない。

EasyMock.exceptLastCall().times()メソッドは、直前のメソッド呼び出しの回数を指定するメソッドである。この例ではopenSource()とcloseSource()の後でtimes(1)を設定しているので、それぞれ1回の呼び出しを想定していることになる。したがってこれらのメソッドが呼ばれなかったり、2回以上呼ばれたりした場合にはテストは失敗する。times(1, 5)のようにした場合には、1回以上5回以下の呼び出しを想定するという意味になる。

EasyMock.exceptLastCall().atLeastOnce()メソッドは、直前のメソッド呼び出しが必ず1回以上行われることを指定するものである。この例の場合には、get()メソッドが1回以上呼ばれることを想定していることになり、1回も呼ばれなかった場合にはテストは失敗する。

実際のテストとしてはgetValues(3)を実行しているので、getValue()が正しく動作するのであれば、まずopenSource()が呼ばれ、続いてget()が3回呼ばれて、最後にcloseSouce()が呼ばれる。これはモックに設定した振る舞い通りの呼び出し順序になっているので、verify()によるチェックは成功するはずである。

もしここで、getValue()内にopenSource()やcloseSource()を忘れるなどのバグがある場合には、verify()によるチェックが通らず、のようなエラーになる。

メソッドの呼び出し順序が間違っているとテストは失敗する

このように、EasyMockではモックの振る舞いに関して様々な検証事項を設定することができるようになっており、様々なテストに適用することが可能である。効果的に利用すれば、テストのための工程を大幅に削減しすることができるだろう。