Getting started with the JMockit Testing Toolkit の邦訳(後編)

モッキング API の使い方

JMockit のモッキング APIJUnitTestNG などの一般的なテスティングフレームワークのテストケースとして使用できます。ここでは JMockit のモッキング API の使いかたを見ていくことにします。mockit パッケージはフルインポートするものとして進めます。

モックオブジェクトの作成

テストクラスでは、単に希望する型のモックフィールドを定義し、JMockit の提供する @Mocked または @Injectable アノテーションを付けるだけです。モック対象の型がクラスの場合、@Injectableアノテーションを適用すると、フィールドに割り当てられたモックが唯一のインスタンスとして振る舞います。

import org.junit.*;
import mockit.*;

public class MyFirstJMockitTest
{
   // アノテーションによりモックオブジェクトが自動で設定される
   @Mocked Collaborator mock1; // 毎回モックがインスタンス化される
   @Injectable AnotherDependency anotherMock; // 唯一のインスタンスのモック

   @Test
   public void myFirstTestMethod()
   {
      // モックフィールドや他のクラスを使用したテストを記述
      // モックフィールドへの再設定は不可
   }

   @Test // メソッドパラメータはデフォルトでモックパラメータとして扱われる
   public void anotherTestMethod(YetAnotherDependency testSpecificMock)
   {
      ...
   }
   ...
}

上記テストクラスの2つ目のテストメソッドでは引数パラメータが宣言されている点が特徴的です。通常 JUnitTestNG のテストメソッドはメソッドに引数パラメータを指定することができません。しかし JMockit では、引数パラメータを定義することでモックを引数として簡単に得ることができます。
モックアノテーションを指定したフィールドは、テストクラスの大部分のメソッドでモックが必要な場合に使うといいでしょう。一方モックパラメータは単体のテストの限られたスコープでモックが必要な場合に利用するのが良いです。JMockit テストランナーによりテストメソッドが起動される毎に、モックフィールドまたはモックパラメータがインスタンス化されてテストクラスに割り当てられます。
モックパラメータには @Mocked または @Injectable アノテーションを付けることもできます。一方アノテーションが付いていないテストクラスのインスタンスフィールドはモック化されることはありません。したがって、クラスフィールドをモック化する場合にはアノテーションを付与する必要があります。

テストメソッドの書き方

振舞に基づいたテストを記載するための Expectations(期待) と Verifications(検証) APIを使った JMockit テストメソッドの基本的な構造を示します。

@Test
public void aTestMethod(<any number of mock parameters>)
{
   // 記録フェーズ  : モックの期待動作の記録。記録すべき内容がない場合は空
   // リプライフェーズ: 記録した動作が再現される。テスト用コードを記載する
   // ベリファイフェーズ: 想定する挙動の検証。検証する内容がない場合は空
}


テストメソッドは 3 つの実行フェーズに分割できます。
最初に 1 つ以上のモック化した型またはモック化したインスタンスの呼び出しを記録します(実際には全ての呼び出しを記録する必要はありません)。
第 2 にテストで扱う製品コード(一般的には単一のテスト対象メソッド呼び出し)を動作させます。前に記録済みのモック化されたメソッドやコンストラクタを呼び出した際の動作が、この段階で再現されることになります。
第 3 にモック化したメソッドやコンストラクタの呼び出しが、リプライの期間で想定通りに行われたかどうかを検証します。このフェーズは厳密な検証が不要な場合(暗黙的に振舞が検証されるか、振舞の確認が不要な場合)は実施しない場合もあります。
ここで モック化されたメソッド/モック化されたコンストラクタ と記述している点に注意してください。JMockit のモッキング API はメソッドとコンストラクタを同等なものとして扱います。これについては後で説明します。

期待する動作の記録

テストの記録フェーズでは、モック化した型/モック化したインスタンスの呼び出しを記録します。この記録はExpectationブロックの中に記載します。

@Test
public void aTestMethod()
{
   new NonStrictExpectations()
   {
      // @Mockedでアノテートされていないローカルなモックフィールド
      MyCollaborator mock;

      {
         mock.getData(); result = "my test data";
         mock.doSomething(anyInt, "some expected value", anyString); times = 1;
      }
   };

   // リプライフェーズでは、テスト対象のメソッドから MyCollaborator インスタンス
   // の getData と doSomething メソッドが呼ばれることが想定される
   ...

   // 検証フェーズでは、任意で MyCollaborator オブジェクトが期待した通りに
   // 呼び出されたかを確認する
   ...
}

Expectation ブロックでは、Expectations と NonStrictExpectations の 2 つのクラスを用いて期待動作を定義します。この 2 つのクラスの違いは、記録する動作に期待するデフォルトの振舞の違いです。
最初のケースでは、ブロック内で定義した期待する動作が厳密に扱われます。これは、想定として記録した動作がリプライフェーズにて、記録された順番通りに呼び出され、記録していないものは呼び出されない、という意味になります。2 つ目の NonStrictExpectations では、期待する動作は常に寛容に扱われます。これは記録した順番や回数がリプライ時に、どんな順番でも何回でも許容されることを意味します。
モックフィールドやモックパラメータに付与できる @NonStrict アノテーションもあり、このアノテーションが付与されている場合は、Expectations クラスのブロック内でも寛容な扱いとなります。

上記に示したテスト中にあるフィールドはどのように割り当てられるのか不思議に思うかもしれません。これは他のモッキング API には見られないものですが、一度そのセマンティクスを理解すれば非常に直感的に感じられるようになります。
result フィールドは直前の呼び出しにより返却される期待値を意味します。times は直前の呼び出しの想定回数を意味します(minTimesとmaxTimesフィールドも用意されています)。
result フィールドは Throwable なインスタンスも受け入れ可能であり、対応する呼び出しがリプライフェーズにて例外かエラーを投げることを期待する という意味となります。

想定内容の検証

賢明な読者は、テストの検証フェーズがなぜ、そしてどんな時に不必要になるのか気付いているかも知れません。
それはテストの期待内容の記録方法が厳格モードか寛容モードのどちらで記録されたかによって決まってきます。
もしテストで使われる全てのモック化された型が厳格な期待動作として記録された場合には、全ての呼び出しはテストの終了によって暗黙的に確かめられることになります。このケースにおいては、あえて検証フェーズを用意する必要がありません。

もしモック化した型に1つでも寛容モードで期待動作を記録した場合には、1つ以上の検証ブロックを使用してリプライフェーズで実際に発生した動作を確認することができます。

@Test
public void aTestMethod(@Injectable final MyCollaborator mock)
{
   // 寛容モードで期待動作を記録 または 全ての期待動作を記録していない場合

   // テスト対象コードの呼び出し
   ...

   new Verifications()
   {{
      // 新しいモック化クラスのインスタンスが無い場合は、引数なしのコンストラクタにより
      // インスタンス化して検証できる
      new MyCollaborator(); times = 0;

      // MyCollaborator#doSomething()が少なくとも1回実行されたことを検証
      mock.doSomething();

      // 他の検証。多くても3回呼び出されたことを検証
      mock.someOtherMethod(anyBoolean, any, withInstanceOf(Xyz.class)); maxTimes = 3;
   }};
}


検証用の API はリッチで柔軟です。上記例の Verifications クラスのほかに実行順序に関与しない検証用くらすなどが利用できます。VerificationsInOrder, FullVerifications, そして FullVerificationsInOrder といったクラスです。InOrder というサフィックスは単に、検証ブロック内での検証順序が、リプライの間で対応する実行順序と相対的に同じであることが必要という意味となります。Full というプレフィックスはリプレイの間、全ての想定内容が検証されることを意味します(暗黙的な検証があるとしても考慮しない)。

状態指向なモック

クライアントまたはテストメソッドからモック化されたコラボレータに渡された引数の値を確認したい場合があります。もちろん記録または想定の検証にてチェックすることができます。しかし、値が複雑な場合など確認が困難な場合があります。

次のテストに示すように、mockit.MockUp というジェネリクスクラスを使うと このようなことが簡単に行えます。

@Test
public void stateBasedTestMethod()
{
   new MockUp<MyCollaborator>()
   {
      @Mock(invocations = 1) // (invocation 回数は任意指定)
      boolean doSomething(int n, String s, ComplexData otherData)
      {
         assertEquals(1, n);
         assertNotNull(otherData);
         ...
         // Return (if non-void) or throw the result we want to produce for the
         // invocation to the mocked method:
         return otherData.isValid();
      }

      // Other mock or regular methods...
   };

   // Exercise the code under test normally; calls to MyCollaborator#doSomething will execute
   // the mock method above; if more or less than one such invocation occurs, the test fails.
   ...
}

@Mock アノテーションはモック化したクラスの対応するメソッドを、モックまたはスタブとして実装とすることをマーキングします。@Mock アノテーションの属性値にて呼び出し回数の制約が指定されていない場合、0回を含むどんな数も許容されます。

さらなる情報源

ここでの例ではモッキングAPIの提供する概要のみを説明しました。さらにあります。JMockit Tutorial にはほぼ全てのメソッド、フィールド、アノテーションなど様々な事柄を広範囲で網羅した説明があります。APIドキュメント にはモッキングAPIの全ての要素についての詳細な仕様があります。
最後にjmockit/samples フォルダにもあるJUnitサンプルテストには数百の利用可能なAPIのサンプルがあります。