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

モック化

  1. 型やインスタンスのモック化
  2. Expectations (期待値)
  3. 記録-再生-検証 モデル
  4. テスト対象クラスのインスタンス化と注入(Injection)
  5. 期待値を記録する
  6. 引数値の柔軟なマッチング
    1. "any" フィールドの利用
    2. "with" メソッドの利用
  7. 実行回数制約の指定
  8. 明示的な検証
    1. 順序付きの検証
    2. 完全な検証
  9. Delegate: 期待値のカスタマイズ
  1. Capturing invocation arguments for verification
    1. Capturing arguments from a single invocation
    2. Capturing arguments from multiple invocations
    3. Capturing new instances
  2. Cascading mocks
    1. Cascading static factory methods
    2. Cascading self-returning methods
  3. Matching invocations to specific instances
    1. Injectable mocked instances
    2. Declaring multiple mocked instances
    3. Instances created with a given constructor
  4. Partial mocking
  5. Mocking unspecified implementation classes

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 Matching invocations to specific instances

Previously, we explained that an expectation recorded on a mocked instance, such as "abc.someMethod();" would actually match invocations to DependencyAbc#someMethod() on any instance of the mocked DependencyAbc class. In most cases, tested code uses a single instance of a given dependency, so this won't really matter and can be safely ignored, whether the mocked instance is passed into the code under test or created inside it. But what if we need to verify that invocations occur on a specific instance, between several ones that happen to be used in the code under test? Also, what if only one or a few instances of the mocked class should actually be mocked, with other instances of the same class remaining unmocked? (This second case tends to occur more often when classes from the standard Java libraries, or from other third-party libraries, are mocked.) The API provides a mocking annotation, @Injectable, which will only mock one instance of the mocked type, leaving others unaffected. Additionally, we have a couple ways to constrain the matching of expectations to specific @Mocked instances, while still mocking all instances of the mocked class.

12.1 Injectable mocked instances

Suppose we need to test code which works with multiple instances of a given class, some of which we want to mock. If an instance to be mocked can be passed or injected into the code under test, then we can declare an @Injectable mock field or mock parameter for it. This @Injectable instance will be an "exclusive" mocked instance; any other instance of the same mocked type, unless obtained from a separate mock field/parameter, will remain as a regular, non-mocked instance.

When using @Injectable, static methods and constructors are also excluded from being mocked. After all, a static method is not associated with any instance of the class, while a constructor is only associated with a newly created (and therefore different) instance.

For an example, lets say we have the following class to be tested.

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();
   }
}

This class could easily be tested without mocking by using ByteArrayInputStream objects for input, but lets say we want to make sure that the InputStream#read() method is properly invoked on each input stream passed in the constructor. The following test will achieve this.

@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);
}

Note that the use of @Injectable is indeed necessary here, since the class under test extends the mocked class, and the method called to exercise ConcatenatingInputStream is actually defined in the base InputStream class. If InputStream was mocked "normally", the read(byte[]) method would always be mocked, regardless of the instance on which it is called.

12.2 Declaring multiple mocked instances

When using @Mocked or @Capturing (and not @Injectable on the same mock field/parameter), we can still match replay invocations to expectations recorded on specific mocked instances. For that, we simply declare multiple mock fields or parameters of the same mocked type, as the next example shows.

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

   // Exercise code under test with mocked instance passed from the test:
   int result = mock.getValue();
   assertEquals(12, result);

   // If another instance is created inside code under test...
   Collaborator another = new Collaborator();

   // ...we won't get the recorded result, but the default one:
   assertEquals(0, another.getValue());
}

The test above will only pass if the tested code (here embedded in the test method itself, for brevity) invokes getValue() on the exact same instance on which the recording invocation was made. This is typically useful when the code under test makes calls on two or more different instances of the same type, and the test wants to verify that a particular invocation occurred on the expected instance.

12.3 Instances created with a given constructor

Specifically for future instances that will later get created by code under test, JMockit provides a couple mechanisms through which we can match invocations on them. Both mechanisms require the recording of an expectation on a specific constructor invocation (a "new" expression) of the mocked class.

The first mechanism involves simply using the new instance obtained from the recorded constructor expectation, when recording expectations on instance methods. Lets see an example.

@Test
public void newCollaboratorsWithDifferentBehaviors(@Mocked Collaborator anyCollaborator) {
   // Record different behaviors for each set of instances:
   new Expectations() {{
      // One set, for instances created with "a value":
      Collaborator col1 = new Collaborator("a value");
      col1.doSomething(anyInt); result = 123;

      // Another set, for instances created with "another value":
      Collaborator col2 = new Collaborator("another value");
      col2.doSomething(anyInt); result = new InvalidStateException();
   }};

   // Code under test:
   new Collaborator("a value").doSomething(5); // will return 123
   ...
   new Collaborator("another value").doSomething(0); // will throw the exception
   ...
}

In the above test, we declare a single mock field or mock parameter of the desired class, using @Mocked. This mock field/parameter, however, is not used when recording expectations; instead, we use the instances created on instantiation recordings to record further expectations on instance methods. The future instances created with matching constructor invocations will map to those recorded instances. Also, note that it's not necessarily a one-to-one mapping, but a many-to-one mapping, from potentially many future instances to a single instance used for recorded expectations.

The second mechanism lets us associate a given mock instance with a recorded constructor expectation, and then use this instance for recording expectations on instance methods which will match invocations on future instances created with a matching constructor invocation. With this alternative mechanism, we can rewrite the test as follows.

@Test
public void newCollaboratorsWithDifferentBehaviors(@Mocked Collaborator col1, @Mocked Collaborator col2) {
   new Expectations() {{
      // Map separate sets of future instances to separate mock parameters:
      new Collaborator("a value"); result = col1;
      new Collaborator("another value"); result = col2;

      // Record different behaviors for each set of instances:
      col1.doSomething(anyInt); result = 123;
      col2.doSomething(anyInt); result = new InvalidStateException();
   }};

   // Code under test:
   new Collaborator("a value").doSomething(5); // will return 123
   ...
   new Collaborator("another value").doSomething(0); // will throw the exception
   ...
}

Both versions of the test are equivalent.

13 Partial mocking

By default, all methods and constructors which can be called on a mocked type and its super-types (except for java.lang.Object) get mocked. This is appropriate for most tests, but in some situations we might need to select only certain methods or constructors to be mocked. Methods/constructors not mocked in an otherwise mocked type will execute normally when called.

When a class or object is partially mocked, JMockit decides whether to execute the real implementation of a method or constructor as it gets called from the code under test, based on which expectations were recorded and which were not. The following example tests will demonstrate it.

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;
      }};

      // Not mocked, as no constructor expectations were recorded:
      Collaborator c1 = new Collaborator();
      Collaborator c2 = new Collaborator(150);

      // Mocked, as a matching method expectation was recorded:
      assertEquals(123, c1.getValue());
      assertEquals(123, c2.getValue());

      // Not mocked:
      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 methods can be dynamically mocked too.
         Collaborator.doSomething(anyBoolean, "test");
      }};

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

      // Not mocked:
      assertEquals(2, collaborator.value);
      assertEquals(45, new Collaborator(45).getValue());
      assertEquals(-1, new Collaborator().getValue());
   }
}

As shown above, the Expectations(Object...) constructor accepts one or more classes or objects to be partially mocked. If a Class object is given, all methods and constructors defined in that class can be mocked, as well as the methods and constructors of its super-classes; all instances of the specified class will be regarded as mocked instances. If, on the other hand, a regular instance is given, then only methods, not constructors, in the class hierarchy can be mocked; even more, only that particular instance will be mocked.

Notice that in these two example tests there is no mock field or mock parameter. The partial mocking constructor effectively provides yet another way to specify mocked types. It also lets us turn objects stored in local variables into mocked instances. Such objects can be created with any amount of state in internal instance fields; they will keep that state when mocked.

It should be noted that, when we request a class or instance to be partially mocked, it can also have invocations verified on it, even if the verified methods/constructors were not recorded. For example, consider the following test.

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

   new Expectations(collaborator) {};

   // No expectations were recorded, so nothing will be mocked.
   int value = collaborator.getValue(); // value == 123
   collaborator.simpleOperation(45, "testing", new Date());
   ...

   // Unmocked methods can still be verified:
   new Verifications() {{ c1.simpleOperation(anyInt, anyString, (Date) any); }};
}

Finally, a simpler way to apply partial mocking to a tested class is to have a field in the test class annotated as both @Tested (see section below) and @Mocked. In this case, the tested object is not passed to the Expectations constructor, but we still need to record expectations on any methods requiring mocked results.

14 Mocking unspecified implementation classes

Our discussion of this feature will be based on the (contrived) code below.

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();
   }
}

The method we want to test, businessOperation(), uses classes that implement a separate interface, Service. One of these implementations is defined through an anonymous inner class, which is completely inaccessible (except for the use of Reflection) from client code.

Given a base type (be it an interface, an abstract class, or any sort of base class), we can write a test which only knows about the base type but where all implementing/extending implementation classes get mocked. To do so, we declare a "capturing" mocked type which refers only to the known base type. Not only will implementation classes already loaded by the JVM get mocked, but also any additional classes that happen to get loaded by the JVM during later test execution. This ability is activated by the @Capturing annotation, which can be applied to mock fields and mock parameters, as demonstrated below.

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);
   }
}

In the test above, two return values are specified for the Service#doSomething() method. This expectation will match all invocations to this method, regardless of the actual instance on which the invocation occurs, and regardless of the actual class implementing the method.