JMockit Java言語のための自動テストツールキット

モック化

  1. 型やインスタンスのモック化
  2. Expectations (期待値)
  3. 記録-再生-検証 モデル
  4. テスト対象クラスのインスタンス化と注入(Injection)
  5. 期待値を記録する
  6. 引数値の柔軟なマッチング
    1. "any" フィールドの利用
    2. "with" メソッドの利用
  7. 実行回数制約の指定
  8. 明示的な検証
    1. 順序付きの検証
    2. 完全な検証
  9. Delegate: 期待値のカスタマイズ
  1. 検証における実行時の引数の取得
    1. 引数の取得(実行は1回だけ)
    2. 引数の取得(実行は複数回)
    3. 生成インスタンスの取得
  2. モックの中のモック(カスケード)
    1. staticなファクトリメソッドのカスケード
    2. 自分自身を返却するメソッドのカスケード
  3. 特定のインスタンスへの呼び出しマッチング
    1. モック化インスタンスの注入
    2. モック化インスタンスを複数宣言する
    3. 指定したコンストラクタでインスタンスを生成する
  4. 部分的なモック化
  5. 実装クラスを指定せずにモック化する

JMockitライブラリでは, Expectations APIが自動テストでのモックの使用を手厚くサポートします. モックを使うことで,依存する他のクラスとの相互作用によって表されるテストしたいコードの振舞いに注目します. モックは,ふつう独立した単体テストの作成で使われ,依存する他クラス等の実装とは独立した単体で実行されます. テストの単位は1クラスであることが多いですが,密接に関連するクラス群を1つのまとまり(ユニット)としてテストすることもできます(例えば,ヘルパークラスをいくつか持っているクラスのようなもの). メソッドをユニットの単位としてテストする,ということまでは一般的には行われません.

ただし,厳密な単体テストは推奨していません.依存関係をひとつひとつモック化しようとするのはやめましょう. モック化は用法用量を守って使うのがベストです.可能ならば,独立した単体テストよりは結合テストを行ったほうがよいでしょう. モック化は結合テストの作成にも役立ちます.一部の依存関係で実際の実装を簡単に使えないとき,モックを使ったほうが明らかにテストしやすいようなときには良いでしょう.

1 型やインスタンスのモック化

モック化は,テストしたいクラスの依存関係(の一部)を分離するための仕組みを提供します. テストクラスに適切な`モックフィールドモックパラメタを宣言することで,どの依存関係をモック化するか指定します. モックフィールドはテストクラス内でアノテーション付きのインスタンス変数(フィールド)として,モックパラメタはテストメソッドにアノテーション付きの引数としてそれぞれ宣言します. モックフィールドやモックパラメタの型は,参照型であればインタフェースでもクラスでも構いません.abstractfinal,アノテーション,enumでも可能です.

下記のテスト雛形(Java 8/JUnit 4)では,モックフィールドとモックパラメタを宣言しつつ,それらをテストコード内でどう使っていくかの典型例を示しています.

@Mocked Dependency mockInstance; // テストケースごとにモック化されたインスタンスが自動生成されます

@Test
public void doBusinessOperationXyz(@Mocked AnotherDependency anotherMock) {
   ...
   new Expectations() {{ // "Expectation"ブロック
      ...
      // 特定の値を返却するよう,期待値の指定(記録):
      mockInstance.mockedMethod(...); result = 123;
      ...
   }};
   ...
   // ここでテスト対象を実行します.
   ...
   new Verifications() {{ // "Verification"ブロック
      // メソッド実行が期待通り行われたか検証
      anotherMock.save(any); times = 1;
   }};
   ...
}

テストメソッドで宣言されたモックパラメタは,JMockitによって自動的にインスタンスが生成され,JUnit/TestNGのテストランナーによってテスト実行時に渡されます.ですから,パラメタはnullにはなりません. モックフィールドについても,自動的に生成され,代入されます(finalを除く).

モックフィールドやパラメタを宣言する方法として,3種類のモックアノテーションがあります. @Mocked は,テスト中に対象クラスのすべてのインスタンス(既存のものだけでなく今後生成されるものを含む)について,メソッドやコンストラクタをモック化します. @Injectable は,単一のモックインスタンスのインスタンスメソッドのみをモック化します. @Capturing, は,モック対象のインタフェースの実装クラスや,モック対象のクラスのサブクラスもモック化します.

JMockitが生成したモックインスタンスは,ふつうテストコードで(期待値の指定(記録)と検証に)利用したり,テスト対象のコードに渡したりします.あるいは使わないかもしれません. 他のモックAPIと異なり,モック化オブジェクトは,必ずしもテスト対象のメソッドへの依存関係の注入に使う必要はありません. @Mocked@Capturing を使用する場合(@Injectableは除きます),JMockitはモック化されたメソッドがどのオブジェクトに対して呼び出されたかを特に意識しません. これは,テスト対象のコード内でnew演算子によって生成されるインスタンスに対しても,透過的にモック化することができる,ということです.生成されるインスタンスは,テストコードで指定されたモッククラスによって置き換えられます.

2 Expectations (期待値)

expectation(期待値)は,対象のテストに関係する具体的なモックメソッド・コンストラクタの呼び出しのセットを表します. 1つの期待値は,あるメソッドやコンストラクタの実行をすべてカバーしていることもありますが,テスト中に実行されるすべての実行を網羅する必要はありません. 期待値で指定したものと実際の呼び出しがマッチするかどうかは,メソッド・コンストラクタのシグネチャだけでなく,呼び出し元のインスタンスや引数の値,そして実行回数といった実行時の状況にもよるでしょう.そのため,期待値に対していくつかのマッチング制約を設定することができます.

引数があるものについては,各引数の厳密な値をマッチング制約として指定できます.例えば,String型の引数について,値が "test string" の場合のみ,というふうに指定すると,その条件にのみマッチするようになります. 後ほど説明するように,厳密な値ではなく,より緩い制約を指定することもできます.

下記の例は, Dependency#someMethod(int, String) に対する期待値を指定しています.厳密な値でマッチするようになっています. 期待値は,モックメソッドを呼び出すような書き方で指定されていることがポイントです.他のモックAPIでよくあるような,特別なAPIは使用しません.ただし,呼び出すような書き方をしても,実際に呼び出しが行われるわけではありません.あくまで,期待値を指定するための記述です.

@Test
public void doBusinessOperationXyz(@Mocked Dependency mockInstance) {
   ...
   new Expectations() {{
      ...
      // インスタンスメソッドの期待値を指定:
      mockInstance.someMethod(1, "test"); result = "mocked";
      ...
   }};

   // テストしたいコードを実行する.
   // 期待値の指定内容とマッチすれば,モックが呼び出される.
}

期待値の指定については,記録再生(リプレイ)検証 それぞれの違いを理解したあとで詳しく説明します.

3 記録-再生-検証 モデル

どのようなテストであれ,テストは少なくとも3つの実行フェーズに分けることができます. 下記に示すように,それぞれのフェーズは順次実行されていきます.

@Test
public void someTestMethod() {
   // 1. 準備: テストに必要なもの
   ...
   // 2. テスト対象の実行(例えばpublicメソッドの実行)
   ...
   // 3. 検証: 正しく実行されたかチェックする
   ...
}

まず,テストに必要なオブジェクトやデータを作成あるいは取得する準備フェーズ.次に,テスト対象の実行.最後に,実行結果と予想結果との比較です.

この3フェーズのモデルは,Arrange, Act, Assert 構文,略して「AAA」とも呼ばれています.

モック型(またはモック化インスタンス)を使った振舞いベースのテストにおいては,この3フェーズに相当する下記のフェーズがあります:

  1. 記録 フェーズ: テストの準備段階,メソッド呼び出しが行われるの,呼び出しの記録
  2. 再生 フェーズ: テストの実行,モック呼び出しが行われる可能性があるフェーズ.記録フェーズでモック化されたメソッドやコンストラクタがここで再生される.ただし,記録と再生は1対1でマッピングされないこともある.
  3. 検証 フェーズ: 期待通りに呼び出しが行われたか検証する.テスト対象の実行が終わったに行われる.

JMockitで記述された振舞いベースのテストは,ふつう次の形式になります:

import mockit.*;
... 他のimport文 ...

public class SomeTest
{
   // このクラスの全テストケースで共通のモックフィールド
   @Mocked Collaborator mockCollaborator;
   @Mocked AnotherDependency anotherDependency;
   ...

   @Test
   public void testWithRecordAndReplayOnly(mock parameters) { // 記録と再生のみ
      // JMockitと無関係なテストの準備(もしあれば)

      new Expectations() {{ // "記録"ブロック
         // モック化したクラスへの呼び出し.実際に呼び出されるわけではなく,期待値が記録される.
         // モック化していないクラスへの呼び出しを記述してもよい(推奨はしない).
      }};

      // テスト対象の実行

      // 検証コード(JUnit/TestNGのアサーション)(もしあれば)
   }

   @Test
   public void testWithReplayAndVerifyOnly(mock parameters) { // 再生と検証のみ
      // JMockitと無関係なテストの準備(もしあれば)

      // テスト対象の実行

      new Verifications() {{ // "検証"ブロック
         // モック化したクラスへの呼び出し.実際に呼び出されるわけではなく,期待値を検証する.
         // モック化していないクラスへの呼び出しを記述してもよい(推奨はしない).
      }};

      // 追加の検証コードがあれば,"検証"ブロックの前後に記述する
   }

   @Test
   public void testWithBothRecordAndVerify(mock parameters) { // 記録と検証の両方
      // JMockitと無関係なテストの準備(もしあれば)

      new Expectations() {{
         // モック化したクラスへの呼び出し.実際に呼び出されるわけではなく,期待値が記録される.
      }};

      // テスト対象の実行

      new VerificationsInOrder() {{ // 順序つきの検証ブロック
         // モック化したクラスへの呼び出し.実際に呼び出されるわけではなく,期待値を検証する.指定した順序で検証する.
      }};

      // 追加の検証コードがあれば,"検証"ブロックの前後に記述する
   }
}

上記の形式の他にもバリエーションはありますが,本質は変わりません.テスト対象の実行より前に書かれた期待値ブロックは記録フェーズであり,その後の期待値ブロックは検証フェーズです. 期待値の記録・検証ブロックは,いくつも記述できます(ゼロでも構いません).

4 テスト対象クラスのインスタンス化と注入(Injection)

final以外の @Testedアノテーションが付いたインスタンスフィールドは,テストメソッド実行の直前に,自動的にインスタンスが生成され,注入(Injection)されます. その時点でフィールドがnullのままになっている場合,インスタンスは適切なコンストラクタを用いて生成され,可能な限り内部の依存関係についても適切に設定されます.

モック化インスタンスをテスト対象のインスタンスに注入するためには,@Injectableアノテーションつきで宣言されたモックフィールドまたはモックパラメタが必要です. @Mockedまたは@Capturingアノテーションのみを指定したモックフィールド・パラメタには注入(Injection)されません. なお,フィールドまたはパラメタがすべてモック可能な型である必要はありません.プリミティブ型や配列でも構いません. 下記のテストクラスで確認してみましょう.

public class SomeTest
{
   @Tested CodeUnderTest tested;
   @Injectable Dependency dep1;
   @Injectable AnotherDependency dep2;
   @Injectable int someIntegralProperty = 123;

   @Test
   public void someTestMethod(@Injectable("true") boolean flag, @Injectable("Mary") String name) {
      // 必要に応じてモック型に期待値を記録する.

      tested.exerciseCodeUnderTest();

      // 必要に応じてモック型の期待値を検証する.
   }
}

モック化しないフィールド・パラメタには,明示的に値を指定する必要があります.そうでない場合,デフォルト値が使われます. 注入(Injection)可能なフィールドの場合,値はフィールドに割当ることが可能です.別の方法として,@Injectableの"value"属性で指定することも可能で,注入可能なテストメソッドのパラメータに対しては唯一の指定方法となります.

注入(Injection)には,コンストラクタ注入とフィールド注入の2つの形式があります. コンストラクタ注入では,テスト対象クラスのコンストラクタの値(引数)が,注入可能なもの,またはテストクラスで利用可能な値で充足できなければなりません. なお,値のセットについては,テストクラスのインスタンスフィールドとして宣言されたものに加えて,テストメソッドのパラメタとして宣言されたものからも利用できることに注意してください.つまり,テストクラス内の各テストケースで,同じテスト対象に異なる値のセットを注入することができます.

テスト対象クラスが指定のコンストラクタで初期化されると,残りの初期化されていないfinal以外のインスタンスフィールドは注入(Injection)対象となります. 注入対象の各フィールドについて,同じ型のフィールド(テストクラス内にある注入可能または既にテスト済みのもの)がないか検索します.1つしかない場合,その値が注入対象のフィールドに格納されます.複数ある場合,注入対象のフィールド名に応じて選択されます.

5 期待値を記録する

void以外の戻り型を持つメソッドでは,resultフィールドに代入することで返却値を記録できます. 再生フェーズでメソッドが呼ばれると,記録した返却値が呼び出し元に返されます. resultへの代入は,Expectationsブロック内に記述したメソッド呼び出しの右に記述します.

メソッド実行時に例外やエラーを発生させたい場合も,resultフィールドにThrowableインスタンスを代入することで実現可能です.コンストラクタにもモックにも適用可能です.

returns(v1, v2, ...)を呼び出すことで,複数の連続する値を期待値として記録することができます. あるいは,resultフィールドに,リスト配列を代入しも同様のことができます.

下記のサンプルでは,ClassUnderTestクラスから呼び出されるDependencyAbcクラスをモック化して,2つの型を返却するメソッドの期待値を記録しています. まずはテスト対象のClassUnderTestクラスの実装を見てみます:

public class ClassUnderTest
{
   private final DependencyAbc abc = new DependencyAbc();

   public void doSomething() {
(1)   int n = abc.intReturningMethod();

      for (int i = 0; i < n; i++) {
         String s;

         try {
(2)         s = abc.stringReturningMethod();
         }
         catch (SomeCheckedException e) {
            // 例外処理的な何か
         }

         // 何か
      }
   }
}

doSomething()に対するテストでは,何度かループを回っている途中に,SomeCheckedExceptionが発生する可能性があります. このクラス間の相互作用に対する期待値を記録するためには,たとえば次のように記述するかもしれません:

@Tested ClassUnderTest cut;

@Test
public void doSomethingHandlesSomeCheckedException(@Mocked DependencyAbc abc) throws Exception {
   new Expectations() {{
(1)   abc.intReturningMethod(); result = 3;

(2)   abc.stringReturningMethod();
      returns("str1", "str2");
      result = new SomeCheckedException();
   }};

   cut.doSomething();
}

このテストでは2つの期待値を記録しています. 1つ目はintReturningMethod()3を返すというもの. 2つ目はstringReturningMethod()が,最初の2回はそれぞれ値を返し,最後(3回目)で例外を発生させるというもの.こうしてテストが目標を達成できるようにします.

6 引数値の柔軟なマッチング

モック対象のメソッド・コンストラクタが引数を持っている場合,期待値の記録や検証に doSomething(1, "s", true); などと記述すると,再生フェーズで値が合致するもののみを対象とすることができます. 引数が通常のオブジェクト(配列やプリミティブ型以外)の場合は,equals(Object)メソッドによって等価であるか判定されます.配列の場合,個々の要素について等価判定を行います.つまり,同じ次元数でそれぞれの要素の等価判定を満たす配列を「等しい」とみなします.

引数については,厳密な値ではなく柔軟なマッチング条件を指定することもできます.その指定には,anyXXX フィールドや withXXX(...) メソッドといった書き方を期待値の記録・検証ブロックで行います.

6.1 "any" フィールドの利用

最も一般的に利用されている引数のマッチング条件は,適切な型に合致する任意の値をマッチさせる,というものです.プリミティブ型とそのラッパークラス,文字列(String),そしてObjectに対し,任意の値にマッチさせる特別な引数マッチング用フィールドを用意しています. 下記のサンプルでは,それをいくつか利用しています:

@Tested CodeUnderTest cut;

@Test
public void someTestMethod(@Mocked DependencyAbc abc) {
   DataItem item = new DataItem(...);

   new Expectations() {{
      // "voidMethod(String, List)" での呼び出しに対応する.
      // 第1引数は任意の文字列,第2引数は任意のリストにマッチする.
      abc.voidMethod(anyString, (List<?>) any);
   }};

   cut.doSomething(item);

   new Verifications() {{
      // 任意のlongまたはLong値での呼び出しにマッチする.
      abc.anotherVoidMethod(anyLong);
   }};
}

"any" フィールドは,実際のメソッドの引数位置と同じ箇所に記載します. ただし,同一の呼び出しの他のパラメタに通常の値を指定することは可能です. 詳細は,お使いのJava IDEでAPIドキュメントを参照してください.

6.2 "with" メソッドの利用

期待値の記録または検証において,メソッドの引数に withXXX(...) というメソッド呼び出しを記述できます.これは,通常の引数値(リテラル,引数など)の指定と組み合わせることも可能です.唯一の注意点は,(withXXXを)メソッドの引数として記述しなきゃいけない,というくらいです. 例えば,withNotEqual(val)メソッドを任意の場所で予め実行しておいて,その結果を変数に代入し,メソッド呼び出しの中で使う… といったことはできません. 下記に "with" メソッドを使ったサンプルを示します:

@Test
public void someTestMethod(@Mocked DependencyAbc abc) {
   DataItem item = new DataItem(...);

   new Expectations() {{
      // "voidMethod(String, List)" の呼び出しのうち,
      // 第1引数が"str"で,第2引数がnull以外のものにマッチする.
      abc.voidMethod("str", (List<?>) withNotNull());

      // DependencyAbc#stringReturningMethod(DataItem, String) の呼び出しのうち,
      // 第1引数が"item"と同一のインスタンスを指していて,第2引数に"xyz"が含まれているものにマッチする.
      abc.stringReturningMethod(withSameInstance(item), withSubstring("xyz"));
   }};

   cut.doSomething(item);

   new Verifications() {{
      // 任意のlong値が指定されたメソッド呼び出しにマッチする.
      abc.anotherVoidMethod(withAny(1L));
   }};
}

このほかにも様々な "with" メソッドがあります.詳しくは,IDEのコード補完機能や,APIドキュメンテーションを参照してください.

JMockitでは,APIで提供しているマッチング条件だけではなく,ユーザが任意のマッチング条件を作成することも可能です(with(Delegate)メソッドを使用します).

7 実行回数制約の指定

期待する(または許容する)実行回数を,実行回数制約によって指定することができます. 制約を指定するためのフィールドとして, times, minTimes, maxTimes の3つが用意されています. これらは期待値の記録及び検証のどちらでも使用できます. どちらで使用した場合も,メソッドやコンストラクタの実行回数が指定した範囲内に収まるよう制限します.実行が期待値を外れて上回ったり下回ったりすると,テストは失敗します. 例を見てみましょう:

@Tested CodeUnderTest cut;

@Test
public void someTestMethod(@Mocked DependencyAbc abc) {
   new Expectations() {{
      // デフォルトで,("minTimes = 1" と記述したのと同様)1回以上実行されることを期待します
      new DependencyAbc();

      // 2回以上実行されることを期待します:
      abc.voidMethod(); minTimes = 2;

      // 1〜5回実行されることを期待します:
      abc.stringReturningMethod(); minTimes = 1; maxTimes = 5;
   }};

   cut.doSomething();
}

@Test
public void someOtherTestMethod(@Mocked DependencyAbc abc) {
   cut.doSomething();

   new Verifications() {{
      // 指定した引数値での実行回数が0〜1回であることを検証します:
      abc.anotherVoidMethod(3); maxTimes = 1;

      // 指定した引数値で1回以上実行されたことを検証します:
      DependencyAbc.someStaticMethod("test", false); // 暗黙的に "minTimes = 1" となります
   }};
}

result フィールドと異なり,これらのフィールドは1回だけ指定可能です.実行回数は非負の数値が指定できます. times = 0 または maxTimes = 0 と指定した場合,マッチするメソッド呼び出しが再生フェーズで1回でも行われるとテストは失敗します.

8 明示的な検証

期待値を記録する過程で実行回数を指定するほかに,テスト実行後の検証ブロックでも実行回数を検証することができます.

"new Verifications() {...}" ブロックでは,"new Expectations() {...}" ブロックと同じAPIが利用可能です(返却値を指定するフィールドを除きます). ですから,anyXXX フィールド,引数のマッチング条件を含むような withXXX(...) メソッド,timesminTimesmaxTimesといった実行回数制約フィールドも使用できます. 下記に例を示します.

@Test
public void verifyInvocationsExplicitlyAtEndOfTest(@Mocked Dependency mock) {
   // 期待値の記録は何もありません.

   // テスト対象コード:
   Dependency dependency = new Dependency();
   dependency.doSomething(123, true, "abc-xyz");

   // Dependency#doSomething(int, boolean, String) メソッドが,
   // 指定した引数で実行されたか検証します.
   new Verifications() {{ mock.doSomething(anyInt, true, withPrefix("abc")); }};
}

何度か説明したように,デフォルトで1回以上実行されたかどうかがチェックされます.実行回数を厳密にチェックしたい場合, times = n を指定する必要があります.

8.1 順序付きの検証

Verifications クラスを使った通常の検証は, 順序付けされていません. 例えば aMethod()anotherMethod() が再生フェーズで実行されたことは検証しますが,どういう順序で実行されたかは検証しません. 実行順序を検証したい場合,代わりに "new VerificationsInOrder() {...}" ブロックを使用してください. このブロックでは,検証したい実行順序で期待値の検証(メソッド呼び出し)を記載してください.

@Test
public void verifyingExpectationsInOrder(@Mocked DependencyAbc abc) {
   // いくつかのテスト対象コード:
   abc.aMethod();
   abc.doSomething("blah", 123);
   abc.anotherMethod(5);
   ...

   new VerificationsInOrder() {{
      // メソッドの実行がここで記載した順序どおりに行われたか検証します.
      abc.aMethod();
      abc.anotherMethod(anyInt);
   }};
}

abc.doSomething(...) は検証されていない ため,いつ実行されても(実行されなくても)かまいません.

8.2 完全な検証

モック化した型・インスタンスへのすべての呼び出しを検証したいときは, "new FullVerifications() {...}" ブロックを使用します.このブロックは,検証されていない呼び出しがないことをチェックします.

@Test
public void verifyAllInvocations(@Mocked Dependency mock) {
   // テスト対象のコード(わかりやすいように記載):
   mock.setSomething(123);
   mock.setSomethingElse("anotherValue");
   mock.setSomething(45);
   mock.save();

   new FullVerifications() {{
      mock.setSomething(anyInt); // 2回の実行がここで検証されます
      mock.setSomethingElse(anyString);
      mock.save(); // いずれかの検証を削除するとテストは失敗します
   }};
}

9 Delegate: 期待値のカスタマイズ

引数によって返却値(期待値)を決めたい場合は,下記のように Delegate オブジェクトを使用することができます:

@Tested CodeUnderTest cut;

@Test
public void delegatingInvocationsToACustomDelegate(@Mocked DependencyAbc anyAbc) {
   new Expectations() {{
      anyAbc.intReturningMethod(anyInt, anyString);
      result = new Delegate() {
         int aDelegateMethod(int i, String s) {
            return i == 1 ? i : s.length();
         }
      };
   }};

   // "intReturningMethod(int, String)" が呼び出されると,記述したdelegateメソッドが実行されます.
   cut.doSomething();
}

Delegate は空のインタフェースで,再生フェーズでdelegateメソッドに処理を委譲するために用意されています.delegateオブジェクトには,任意の名前の非privateメソッドを1つだけ記述できます. delegateメソッドのパラメタについては,記録したいメソッドと同じものにするか,または引数なしで記述してください. どちらの記述方法でも,delegateメソッドは追加のパラメタとして先頭(第1引数)にInvocation型の値を受け取ることができます. Invocation オブジェクトを使用すると,呼び出されたインスタンスや実際のパラメタなどの属性にアクセスすることができます. 返却値の型は記録したいメソッドと同じにする必要はありませんが,ClassCastExceptionを防ぐために互換性のある型にしましょう.

コンストラクタもdelegateメソッドによってカスタマイズできます. 下記の例では,コンストラクタが呼び出されるとdelegateによって特定の条件で例外を発生させるようにしてあります.

@Test
public void delegatingConstructorInvocations(@Mocked Collaborator anyCollaboratorInstance) {
   new Expectations() {{
      new Collaborator(anyInt);
      result = new Delegate() {
         void delegate(int i) { if (i < 1) throw new IllegalArgumentException(); }
      };
   }};

   // "Collaborator(int)" によるインスタンス生成で上記delegateが実行されます
   new Collaborator(4);
}

10 検証における実行時の引数の取得

実行時の引数は,特殊な "withCapture(...)" メソッドによって検証時に取得することができます.3つのケースに応じてそれぞれキャプチャ方法が用意されています:

  1. 1回の実行について,モック化したメソッドの引数を検証する: T withCapture();
  2. 複数の実行について,モック化したメソッドの引数を検証する: T withCapture(List<T>);
  3. モック化したコンストラクタの引数を検証する: List<T> withCapture(T).

10.1 引数の取得(実行は1回だけ)

1回の実行についてモック化したメソッド・コンストラクタの引数を取得する場合, withCapture() を使用します.下記に例を示します.

@Test
public void capturingArgumentsFromSingleInvocation(@Mocked Collaborator mock) {
   // テスト対象のコードがこうだとします:
   ...
   new Collaborator().doSomething(0.5, new int[2], "test");

   // テストコードはこんな感じです:
   new Verifications() {{
      double d;
      String s;
      mock.doSomething(d = withCapture(), null, s = withCapture());

      assertTrue(d > 0.0);
      assertTrue(s.length() > 1);
   }};
}

withCapture() メソッドは検証ブロックでのみ利用可能です. ふつう,1回だけ実行されると予想される場合に使用します.複数回実行された場合は,上書きされて最後の値が設定されます. この動作は,JPAの@Entityのような複雑な型を引数に持つケースで役に立つかもしれません.

10.2 引数の取得(実行は複数回)

複数回の実行が予想され,複数回それぞれの引数値を取得したい場合は, withCapture(List) メソッドを使います.下記に例を示します.

@Test
public void capturingArgumentsFromMultipleInvocations(@Mocked Collaborator mock) {
   // テスト対象のコードがこうだとします:
   mock.doSomething(dataObject1);
   mock.doSomething(dataObject2);
   ...

   // テストコードはこうです:
   new Verifications() {{
      List<DataObject> dataObjects = new ArrayList<>();
      mock.doSomething(withCapture(dataObjects));

      assertEquals(2, dataObjects.size());
      DataObject data1 = dataObjects.get(0);
      DataObject data2 = dataObjects.get(1);
      // Perform arbitrary assertions on data1 and data2.
   }};
}

withCapture() と異なり, withCapture(List) のオーバーロードメソッドは期待値の記録ブロックでも利用可能です.

10.3 生成インスタンスの取得

最後に,モック化クラスの新規インスタンス(インスタンス生成)を取得することも可能です.

@Test
public void capturingNewInstances(@Mocked Person mockedPerson) {
   // テスト対象のコード:
   dao.create(new Person("Paul", 10));
   dao.create(new Person("Mary", 15));
   dao.create(new Person("Joe", 20));
   ...

   // テストコード:
   new Verifications() {{
      // 指定したコンストラクタで生成されたインスタンスを取得
      List<Person> personsInstantiated = withCapture(new Person(anyString, anyInt));

      // メソッド呼び出しの中で生成されたインスタンスを取得
      List<Person> personsCreated = new ArrayList<>();
      dao.create(withCapture(personsCreated));

      // 生成されたインスタンス群が同一かどうかをチェック
      assertEquals(personsInstantiated, personsCreated);
   }};
}

11 モックの中のモック(カスケード)

複数のオブジェクトによって機能する複雑なAPIを使用する場合, obj1.getObj2(...).getYetAnotherObj().doSomething(...) というようにメソッドチェーン的な呼び出しをすることも少なくありません. この場合,obj1から始まるチェーンの全オブジェクト・クラスをモック化する必要が生じるかもしれません.

3つのモックアノテーションのいずれでもこれを実現することができます. 下記のサンプルは, java.netjava.nio のAPIを用いた簡単な例です.

@Test
public void recordAndVerifyExpectationsOnCascadedMocks(
   @Mocked Socket anySocket, // テスト中に生成されるSocketはすべてモックに
   @Mocked SocketChannel cascadedChannel // Socketから生成されるであろうクラスもモックに
) throws Exception {
   new Expectations() {{
      // Socket#getChannel() を呼び出すと,Socket内部の SocketChannel が返却されますが,
      // そのインスタンスは本メソッドの第2引数に与えられたモックパラメタになります.
      // 下記の指定により,そのSocketChannelに対する期待値を与えることが可能です.
      cascadedChannel.isConnected(); result = false;
   }};

   // テスト対象のコード:
   Socket sk = new Socket(); // "anySocket" でモック化
   SocketChannel ch = sk.getChannel(); // "cascadedChannel" でモック化

   if (!ch.isConnected()) {
      SocketAddress sa = new InetSocketAddress("remoteHost", 123);
      ch.connect(sa);
   }

   InetAddress adr1 = sk.getInetAddress();  // 新しいInetAddressインスタンス
   InetAddress adr2 = sk.getLocalAddress(); // また別のインスタンス
   ...

   // テストコード:
   new Verifications() {{ cascadedChannel.connect((SocketAddress) withNotNull()); }};
}

上記のテストでは,モック化した Socket クラスの特定のメソッドがテストで呼び出されるたびに, カスケードされたモック(モック内部のモック) を返却します. カスケードされたモックはさらにカスケードすることが出来るため,参照型を返すメソッドがnull参照を返却することはありません(ObjectStringでnullを返却する場合や,モック化していない空のコレクションを返す場合を除きます).

モックフィールド・パラメタのなかに(上記のcascadedChannelのような)利用可能なモックインスタンスが存在しない場合は,各モックメソッドの初回実行時に新しくカスケードされたモックインスタンスが生成されます. 上記の例では,同じInetAddress型を返却する2つのメソッドがありますが,メソッドが異なるためそれぞれ別々のインスタンスを生成・返却しています.同じメソッドを呼び出した場合,同じインスタンスが返却されます.

新しく作られるカスケードされたインスタンスは,テスト内のほかの(同一型の)インスタンスに影響しないよう,@Injectableとして振舞います.

なお,必要であれば,カスケードされたインスタンスをモック化しない,または別々のモックインスタンスにする,または全く返却しないということも可能です. そうしたい場合は,resultフィールドに返却したいインスタンスを指定するか,(全く返却しない場合は)nullを指定します.

11.1 staticなファクトリメソッドのカスケード

カスケードは,staticファクトリメソッドを含むクラスをモック化する場合に非常に便利です. 下記のサンプルでは,JSF(Java EE)のjavax.faces.context.FacesContextクラスをモック化しようとしています.

@Test
public void postErrorMessageToUIForInvalidInputFields(@Mocked FacesContext jsf) {
   // 何か無効な入力がされたとする

   // テスト対象のコードは,JSFページの入力フィールドをバリデーションして,
   // 失敗した場合はエラーメッセージをJSFのコンテキストに設定する.
   FacesContext ctx = FacesContext.getCurrentInstance();

   if (何らかの入力がバリデーションエラーだったら) {
      ctx.addMessage(null, new FacesMessage("入力 xyz に誤りがあります: なんとかかんとか..."));
   }
   ...

   // テストコード: コンテキストにエラーメッセージが設定されたか検証する
   new Verifications() {{
      FacesMessage msg;
      jsf.addMessage(null, msg = withCapture());
      assertTrue(msg.getSummary().contains("なんとかかんとか"));
   }};
}

上記サンプルの面白いところは,FacesContext.getCurrentInstance() が自動的にモック化インスタンスの "jsf" を返却するため,モック化について何も気にする必要がないという点です.

11.2 自分自身を返却するメソッドのカスケード

カスケードは,これまで説明した以外に,"fluent interface"を使ったコードでも役に立ちます."builder"オブジェクトは,ほとんどのメソッドで自分自身を返却するため,生成したいオブジェクト・状態を指定するメソッドチェーンが出来上がります. 下記のサンプルでは,java.lang.ProcessBuilderクラスをモック化しています.

@Test
public void createOSProcessToCopyTempFiles(@Mocked ProcessBuilder pb) throws Exception {
   // テスト対象のコードでは,新しいプロセスを作成しOS依存のコマンドを実行する
   String cmdLine = "copy /Y *.txt D:\\TEMP";
   File wrkDir = new File("C:\\TEMP");
   Process copy = new ProcessBuilder().command(cmdLine).directory(wrkDir).inheritIO().start();
   int exit = copy.waitFor();
   ...

   // プロセスが正しいコマンド情報をもって生成されたことを検証する
   new Verifications() {{ pb.command(withSubstring("copy")).start(); }};
}

上記では,command(...)directory(...)inheritIO() の3メソッドで作成するプロセスの設定を行い, start() メソッドでプロセスを作成しています. モック化したプロセスビルダーオブジェクトは,これらのメソッドで自分自身("pb")を返却します.また,start()が実行されると新たなモック化 Process が返却されます.

12 特定のインスタンスへの呼び出しマッチング

ここまで,モックインスタンスによる期待値の記録を説明してきました. "abc.someMethod();" は, モック化されたDependencyAbc クラスの 任意のインスタンスの DependencyAbc#someMethod() の実行にマッチします. ほとんどの場合,テスト対象のコードは依存関係のひとつのインスタンスを使うだけで,モック化インスタンスがテスト対象に渡される内部で作成されるかは特に気にする必要はありません. しかし,特定の場所で使用される特定のインスタンスの呼び出しを検証したいときはどうするのか? また,クラスのインスタンスの一部だけをモック化して,残りをモック化したくないときはどうするのか?(この問題は,Javaの標準ライブラリやサードパーティライブラリをモック化するときにしばしば発生します) 本APIでは,@Injectableというモック化アノテーションを提供していて,1つのインスタンスだけをモック化して,残りをモック化しないということが可能です.さらに,@Mockedによってあるクラスのすべてのインスタンスをモック化しつつ,特定のインスタンスにのみ期待値のマッチングを制限する方法もあります.

12.1 モック化インスタンスの注入

とあるクラスの複数のインスタンスが動作するコードで,そのうちいくつかをモック化してテストしたいとしましょう. @Injectable モックフィールド・パラメタを宣言することで,テスト対象のコードにモック化インスタンスを渡す(注入する)こどができます.この@Injectable インスタンスは「排他的」なモック化インスタンスになります.同じクラス(型)のその他のインスタンスは,別途モックフィールド・パラメタを与えない限りは,モック化されていない普通のインスタンスになります.

@Injectable を使用した場合,staticメソッドやコンストラクタはモック化されません.なぜかというと,static メソッドはどのインスタンスにも結びついていませんし,コンストラクタは新たに作成される(つまりモックとは別の)インスタンスにのみ結びついているからです.

例えば,テストしたいクラスが下記のようになっているとします.

public final class ConcatenatingInputStream extends InputStream
{
   private final Queue<InputStream> sequentialInputs;
   private InputStream currentInput;

   public ConcatenatingInputStream(InputStream... sequentialInputs) {
      this.sequentialInputs = new LinkedList<InputStream>(Arrays.asList(sequentialInputs));
      currentInput = this.sequentialInputs.poll();
   }

   @Override
   public int read() throws IOException {
      if (currentInput == null) return -1;

      int nextByte = currentInput.read();

      if (nextByte >= 0) {
         return nextByte;
      }

      currentInput = sequentialInputs.poll();
      return read();
   }
}

このクラスは,ByteArrayInputStream オブジェクトを入力に与えることで,モック化せずに簡単にテストできます.しかし,コンストラクタの引数に渡した全ストリームに対し, InputStream#read() メソッドが呼び出されているか確認したいとするとどうでしょう.下記のようにすると,実現できます.

@Test
public void concatenateInputStreams(
   @Injectable InputStream input1, @Injectable InputStream input2
) throws Exception {
   new Expectations() {{
      input1.read(); returns(1, 2, -1);
      input2.read(); returns(3, -1);
   }};

   InputStream concatenatedInput = new ConcatenatingInputStream(input1, input2);
   byte[] buf = new byte[3];
   concatenatedInput.read(buf);

   assertArrayEquals(new byte[] {1, 2, 3}, buf);
}

@Injectableは必要です.テスト対象のクラス ConcatenatingInputStreamInputStreamのサブクラスとして定義されているからです. もしInputStreamを全部モック化してしまうと,read(byte[])メソッドは,どのインスタンスで実行されているかに関係なくモック化されてしまいます.

12.2 モック化インスタンスを複数宣言する

@Mocked@Capturing を使用した(同じモックフィールド・パラメタに@Injectable を使用しなかった)場合でも,再生フェーズでの呼び出しを特定のモック化インスタンスの期待値にマッチングさせることができます. そのためには,次のように同じ型のモックフィールド・パラメタを複数宣言します.

@Test
public void matchOnMockInstance(
   @Mocked Collaborator mock, @Mocked Collaborator otherInstance
) {
   new Expectations() {{ mock.getValue(); result = 12; }};

   // テスト対象のコード(モック化インスタンスを使う):
   int result = mock.getValue();
   assertEquals(12, result);

   // テスト対象のコード(別のインスタンスが作成されたとすると):
   Collaborator another = new Collaborator();

   // 記録していた期待値とは違う値になる(はず)
   assertEquals(0, another.getValue());
}

上記のテストは,テスト対象のコード(ここではわかりやすいようにテストコードに埋め込んであります)が getValue() を呼び出すときに,期待値を記録したインスタンスと同じインスタンスだった場合にだけ期待値が使用されます. これは,テスト対象のコードが同じ型のインスタンスを持っているときに,特定のインスタンスで呼び出しが発生したか検証したい場合に役立ちます.

12.3 指定したコンストラクタでインスタンスを生成する

JMockitでは,テスト対象のコードで生成されるであろう将来のインスタンスに対しても,メソッド呼び出しにマッチングさせることができる機能を2つ提供しています. 2つとも,モック化するクラスのコンストラクタ呼び出し("new"式)に対する期待値を記録する必要があります.

1つ目の方法は,インスタンスメソッドへの期待値を記録するときに,期待値の記録ブロックで生成したインスタンスに対してそれを行う,というものです.以下に例を示します.

@Test
public void newCollaboratorsWithDifferentBehaviors(@Mocked Collaborator anyCollaborator) {
   // それぞれのインスタンスに異なる期待値を記録する
   new Expectations() {{
      // 1つ目は"a value"という引数で生成されたインスタンスに対するもの
      Collaborator col1 = new Collaborator("a value");
      col1.doSomething(anyInt); result = 123;

      // 2つ目は"another value"という引数で生成されたインスタンスに対するもの
      Collaborator col2 = new Collaborator("another value");
      col2.doSomething(anyInt); result = new InvalidStateException();
   }};

   // テスト対象のコード:
   new Collaborator("a value").doSomething(5); // 123 が返却される
   ...
   new Collaborator("another value").doSomething(0); // 例外が発生する
   ...
}

上記のテストでは,モック化したいクラスのモックフィールド(またはパラメタ)を@Mockedを使って1つだけ宣言します. しかし,宣言したモックフィールド(パラメタ)は期待値の記録では使用しません.かわりに,記録ブロックでインスタンス化したインスタンスに対して,インスタンスメソッドの期待値の記録を行います. その後生成されるインスタンスは,コンストラクタの呼び出しがマッチした場合に限って期待値を記録したインスタンスにマッピングされます. なお,マッピングは必ずしも1対1ではなく,(コンストラクタの呼び出しがマッチすれば)多対1となる可能性があるので注意してください.

2つ目の方法は,(モックフィールド・パラメタにある)モック化インスタンスをコンストラクタ呼び出しのマッチングに割り当てるようにして,モック化インスタンスに期待値を記録しておく方法です. この方法を使うと,テストは次のように書き直すことができます.

@Test
public void newCollaboratorsWithDifferentBehaviors(@Mocked Collaborator col1, @Mocked Collaborator col2) {
   new Expectations() {{
      // コンストラクタの引数によって,モックパラメタを使い分ける
      new Collaborator("a value"); result = col1;
      new Collaborator("another value"); result = col2;

      // それぞれのモック化インスタンス(モックパラメタ)に別々の期待値を記録する
      col1.doSomething(anyInt); result = 123;
      col2.doSomething(anyInt); result = new InvalidStateException();
   }};

   // テスト対象のコード:
   new Collaborator("a value").doSomething(5); // 12 が返却される
   ...
   new Collaborator("another value").doSomething(0); // 例外が発生する
   ...
}

どちらの書き方も等価(同じ動き)です.

13 部分的なモック化

デフォルトでは、モック対象のクラスそのスーパータイプ(java.lang.Objectを除きます)のすべてのメソッドとコンストラクタがモックになります. ほとんどのテストでこれは問題になりませんが,場合によっては特定のメソッドやコンストラクタのみをモック化したいこともあるかもしれません. そうすれば,モック化していないメソッドやコンストラクタでは,通常の(従来の)処理を実行することができます.

クラスやオブジェクトの一部がモック化されている場合,JMockitはもとの実装を実行するのか記録されている期待値を用いるのかを判断します. 下記に例を示します.

public class PartialMockingTest
{
   static class Collaborator
   {
      final int value;

      Collaborator() { value = -1; }
      Collaborator(int value) { this.value = value; }

      int getValue() { return value; }
      final boolean simpleOperation(int a, String b, Date c) { return true; }
      static void doSomething(boolean b, String s) { throw new IllegalStateException(); }
   }

   @Test
   public void partiallyMockingAClassAndItsInstances() {
      Collaborator anyInstance = new Collaborator();

      new Expectations(Collaborator.class) {{
         anyInstance.getValue(); result = 123;
      }};

      // モックではない.コンストラクタの期待値は記録されていないためです.
      Collaborator c1 = new Collaborator();
      Collaborator c2 = new Collaborator(150);

      // モック.記録しているメソッド呼び出しにマッチするためです.
      assertEquals(123, c1.getValue());
      assertEquals(123, c2.getValue());

      // モックではない.
      assertTrue(c1.simpleOperation(1, "b", null));
      assertEquals(45, new Collaborator(45).value);
   }

   @Test
   public void partiallyMockingASingleInstance() {
      Collaborator collaborator = new Collaborator(2);

      new Expectations(collaborator) {{
         collaborator.getValue(); result = 123;
         collaborator.simpleOperation(1, "", null); result = false;

         // staticメソッドもモック化できます.
         Collaborator.doSomething(anyBoolean, "test");
      }};

      // モック:
      assertEquals(123, collaborator.getValue());
      assertFalse(collaborator.simpleOperation(1, "", null));
      Collaborator.doSomething(true, "test");

      // モックではない:
      assertEquals(2, collaborator.value);
      assertEquals(45, new Collaborator(45).getValue());
      assertEquals(-1, new Collaborator().getValue());
   }
}

上記に示したように, Expectations(Object...) コンストラクタは部分的にモック化したいクラスやオブジェクトを指定できます. Class オブジェクトを与えた場合,すべてのメソッドコンストラクタ(親クラスのメソッドやコンストラクタも含む)をモック化できます.指定したクラスのすべてのインスタンスは,モック化インスタンスとみなされます. 一方,通常のインスタンスを与えた場合,クラス階層にあるメソッドのみをモック化でき,コンストラクタはモック化できません.また,与えたインスタンスのみがモック化されます.

上記2つの例では,モックフィールドやパラメタは使用していません. 部分モックのコンストラクタは,モック化する型を指定する(これまでと)別の方法を提供しています.また,ローカル変数に格納されているオブジェクトをモック化することも可能です. こうしたオブジェクトは,インスタンスフィールドにいくつも作成できます.インスタンスの状態はモック化しても保持されます.

なお,クラスやインスタンスを部分的にモック化した場合でも,期待値を記録していないメソッドやコンストラクタの呼び出しを検証することが出来ます. 例えば下記のテストを考えてみます.

@Test
public void partiallyMockingAnObjectJustForVerifications() {
   Collaborator collaborator = new Collaborator(123);

   new Expectations(collaborator) {};

   // 期待値は記録しておらず,何もモック化されません.
   int value = collaborator.getValue(); // value == 123
   collaborator.simpleOperation(45, "testing", new Date());
   ...

   // モック化していないメソッドも検証ができます:
   new Verifications() {{ c1.simpleOperation(anyInt, anyString, (Date) any); }};
}

最後に,テスト対象のクラスを部分的にモック化するより簡単な方法として,@Tested (4章参照) と @Mocked の両方のアノテーションをつける方法があります. この場合,テスト対象のオブジェクトは Expectations コンストラクタの引数には指定しませんが,モック化したいメソッドへの期待値の記録は必要になります.

14 実装クラスを指定せずにモック化する

この章の話は,下記のコードをベースとしたものです.

public interface Service { int doSomething(); }
final class ServiceImpl implements Service { public int doSomething() { return 1; } }

public final class TestedUnit
{
   private final Service service1 = new ServiceImpl();
   private final Service service2 = new Service() { public int doSomething() { return 2; } };

   public int businessOperation() {
      return service1.doSomething() + service2.doSomething();
   }
}

テストしたいメソッド businessOperation() は,インタフェース Service を実装した複数のクラスを使用しています. 実装クラスの1つは,(Reflectionを除き)外部からアクセスできない匿名の内部クラスによって定義されています.

基底の型(interfaceabstractクラス,あるいは何らかの親クラス)をモック対象に指定することで,基底の型を知っていれば,これを実装/継承するすべてのクラスをモック化することが可能です. これを実現するには,基底の型に対して "capturing" モック型を宣言します.JVMによって既にロードされている実装クラスだけでなく,テスト中にロードされるクラスも,モック化されます. この機能は,モックフィールドやパラメタに @Capturing アノテーションを指定することで有効にできます.下記に例を示します.

public final class UnitTest
{
   @Capturing Service anyService;

   @Test
   public void mockingImplementationClassesFromAGivenBaseType() {
      new Expectations() {{ anyService.doSomething(); returns(3, 4); }};

      int result = new TestedUnit().businessOperation();

      assertEquals(7, result);
   }
}

上記のテストでは, Service#doSomething() メソッドに2つの返却値が指定されています. この指定は,インスタンス実装クラスがどういうものかに関係なく,このメソッドのすべての呼び出しにマッチします.