JMockitライブラリでは, Expectations APIが自動テストでのモックの使用を手厚くサポートします. モックを使うことで,依存する他のクラスとの相互作用によって表されるテストしたいコードの振舞いに注目します. モックは,ふつう独立した単体テストの作成で使われ,依存する他クラス等の実装とは独立した単体で実行されます. テストの単位は1クラスであることが多いですが,密接に関連するクラス群を1つのまとまり(ユニット)としてテストすることもできます(例えば,ヘルパークラスをいくつか持っているクラスのようなもの). メソッドをユニットの単位としてテストする,ということまでは一般的には行われません.
ただし,厳密な単体テストは推奨していません.依存関係をひとつひとつモック化しようとするのはやめましょう. モック化は用法用量を守って使うのがベストです.可能ならば,独立した単体テストよりは結合テストを行ったほうがよいでしょう. モック化は結合テストの作成にも役立ちます.一部の依存関係で実際の実装を簡単に使えないとき,モックを使ったほうが明らかにテストしやすいようなときには良いでしょう.
モック化は,テストしたいクラスの依存関係(の一部)を分離するための仕組みを提供します.
テストクラスに適切な`モックフィールドやモックパラメタを宣言することで,どの依存関係をモック化するか指定します.
モックフィールドはテストクラス内でアノテーション付きのインスタンス変数(フィールド)として,モックパラメタはテストメソッドにアノテーション付きの引数としてそれぞれ宣言します.
モックフィールドやモックパラメタの型は,参照型であればインタフェースでもクラスでも構いません.abstract
やfinal
,アノテーション,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
演算子によって生成されるインスタンスに対しても,透過的にモック化することができる,ということです.生成されるインスタンスは,テストコードで指定されたモッククラスによって置き換えられます.
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つの実行フェーズに分けることができます. 下記に示すように,それぞれのフェーズは順次実行されていきます.
@Test
public void someTestMethod() {
// 1. 準備: テストに必要なもの
...
// 2. テスト対象の実行(例えばpublicメソッドの実行)
...
// 3. 検証: 正しく実行されたかチェックする
...
}
まず,テストに必要なオブジェクトやデータを作成あるいは取得する準備フェーズ.次に,テスト対象の実行.最後に,実行結果と予想結果との比較です.
この3フェーズのモデルは,Arrange, Act, Assert 構文,略して「AAA」とも呼ばれています.
モック型(またはモック化インスタンス)を使った振舞いベースのテストにおいては,この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() {{ // 順序つきの検証ブロック
// モック化したクラスへの呼び出し.実際に呼び出されるわけではなく,期待値を検証する.指定した順序で検証する.
}};
// 追加の検証コードがあれば,"検証"ブロックの前後に記述する
}
}
上記の形式の他にもバリエーションはありますが,本質は変わりません.テスト対象の実行より前に書かれた期待値ブロックは記録フェーズであり,その後の期待値ブロックは検証フェーズです. 期待値の記録・検証ブロックは,いくつも記述できます(ゼロでも構いません).
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つしかない場合,その値が注入対象のフィールドに格納されます.複数ある場合,注入対象のフィールド名に応じて選択されます.
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回目)で例外を発生させるというもの.こうしてテストが目標を達成できるようにします.
モック対象のメソッド・コンストラクタが引数を持っている場合,期待値の記録や検証に doSomething(1, "s", true);
などと記述すると,再生フェーズで値が合致するもののみを対象とすることができます.
引数が通常のオブジェクト(配列やプリミティブ型以外)の場合は,equals(Object)
メソッドによって等価であるか判定されます.配列の場合,個々の要素について等価判定を行います.つまり,同じ次元数でそれぞれの要素の等価判定を満たす配列を「等しい」とみなします.
引数については,厳密な値ではなく柔軟なマッチング条件を指定することもできます.その指定には,anyXXX
フィールドや withXXX(...)
メソッドといった書き方を期待値の記録・検証ブロックで行います.
最も一般的に利用されている引数のマッチング条件は,適切な型に合致する任意の値をマッチさせる,というものです.プリミティブ型とそのラッパークラス,文字列(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ドキュメントを参照してください.
期待値の記録または検証において,メソッドの引数に 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)
メソッドを使用します).
期待する(または許容する)実行回数を,実行回数制約によって指定することができます.
制約を指定するためのフィールドとして, 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回でも行われるとテストは失敗します.
期待値を記録する過程で実行回数を指定するほかに,テスト実行後の検証ブロックでも実行回数を検証することができます.
"new Verifications() {...}
" ブロックでは,"new Expectations() {...}
" ブロックと同じAPIが利用可能です(返却値を指定するフィールドを除きます).
ですから,anyXXX
フィールド,引数のマッチング条件を含むような withXXX(...)
メソッド,times
,minTimes
,maxTimes
といった実行回数制約フィールドも使用できます.
下記に例を示します.
@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
を指定する必要があります.
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(...)
は検証されていない ため,いつ実行されても(実行されなくても)かまいません.
モック化した型・インスタンスへのすべての呼び出しを検証したいときは,
"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(); // いずれかの検証を削除するとテストは失敗します
}};
}
引数によって返却値(期待値)を決めたい場合は,下記のように 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);
}
実行時の引数は,特殊な "withCapture(...)
" メソッドによって検証時に取得することができます.3つのケースに応じてそれぞれキャプチャ方法が用意されています:
T withCapture()
;T withCapture(List<T>)
;List<T> withCapture(T)
.
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
のような複雑な型を引数に持つケースで役に立つかもしれません.
複数回の実行が予想され,複数回それぞれの引数値を取得したい場合は,
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)
のオーバーロードメソッドは期待値の記録ブロックでも利用可能です.
最後に,モック化クラスの新規インスタンス(インスタンス生成)を取得することも可能です.
@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);
}};
}
複数のオブジェクトによって機能する複雑なAPIを使用する場合, obj1.getObj2(...).getYetAnotherObj().doSomething(...)
というようにメソッドチェーン的な呼び出しをすることも少なくありません.
この場合,obj1
から始まるチェーンの全オブジェクト・クラスをモック化する必要が生じるかもしれません.
3つのモックアノテーションのいずれでもこれを実現することができます.
下記のサンプルは, java.net
と java.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
参照を返却することはありません(Object
やString
でnullを返却する場合や,モック化していない空のコレクションを返す場合を除きます).
モックフィールド・パラメタのなかに(上記のcascadedChannel
のような)利用可能なモックインスタンスが存在しない場合は,各モックメソッドの初回実行時に新しくカスケードされたモックインスタンスが生成されます.
上記の例では,同じInetAddress
型を返却する2つのメソッドがありますが,メソッドが異なるためそれぞれ別々のインスタンスを生成・返却しています.同じメソッドを呼び出した場合,同じインスタンスが返却されます.
新しく作られるカスケードされたインスタンスは,テスト内のほかの(同一型の)インスタンスに影響しないよう,@Injectable
として振舞います.
なお,必要であれば,カスケードされたインスタンスをモック化しない,または別々のモックインスタンスにする,または全く返却しないということも可能です.
そうしたい場合は,result
フィールドに返却したいインスタンスを指定するか,(全く返却しない場合は)null
を指定します.
カスケードは,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
" を返却するため,モック化について何も気にする必要がないという点です.
カスケードは,これまで説明した以外に,"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
が返却されます.
ここまで,モックインスタンスによる期待値の記録を説明してきました.
"abc.someMethod();
" は, モック化されたDependencyAbc
クラスの 任意のインスタンスの DependencyAbc#someMethod()
の実行にマッチします.
ほとんどの場合,テスト対象のコードは依存関係のひとつのインスタンスを使うだけで,モック化インスタンスがテスト対象に渡されるか内部で作成されるかは特に気にする必要はありません.
しかし,特定の場所で使用される特定のインスタンスの呼び出しを検証したいときはどうするのか? また,クラスのインスタンスの一部だけをモック化して,残りをモック化したくないときはどうするのか?(この問題は,Javaの標準ライブラリやサードパーティライブラリをモック化するときにしばしば発生します)
本APIでは,@Injectable
というモック化アノテーションを提供していて,1つのインスタンスだけをモック化して,残りをモック化しないということが可能です.さらに,@Mocked
によってあるクラスのすべてのインスタンスをモック化しつつ,特定のインスタンスにのみ期待値のマッチングを制限する方法もあります.
とあるクラスの複数のインスタンスが動作するコードで,そのうちいくつかをモック化してテストしたいとしましょう.
@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
は必要です.テスト対象のクラス ConcatenatingInputStream
はInputStream
のサブクラスとして定義されているからです.
もしInputStream
を全部モック化してしまうと,read(byte[])
メソッドは,どのインスタンスで実行されているかに関係なくモック化されてしまいます.
@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()
を呼び出すときに,期待値を記録したインスタンスと同じインスタンスだった場合にだけ期待値が使用されます.
これは,テスト対象のコードが同じ型のインスタンスを持っているときに,特定のインスタンスで呼び出しが発生したか検証したい場合に役立ちます.
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); // 例外が発生する
...
}
どちらの書き方も等価(同じ動き)です.
デフォルトでは、モック対象のクラスとそのスーパータイプ(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
コンストラクタの引数には指定しませんが,モック化したいメソッドへの期待値の記録は必要になります.
この章の話は,下記のコードをベースとしたものです.
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を除き)外部からアクセスできない匿名の内部クラスによって定義されています.
基底の型(interface
やabstract
クラス,あるいは何らかの親クラス)をモック対象に指定することで,基底の型を知っていれば,これを実装/継承するすべてのクラスをモック化することが可能です.
これを実現するには,基底の型に対して "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つの返却値が指定されています.
この指定は,インスタンスや実装クラスがどういうものかに関係なく,このメソッドのすべての呼び出しにマッチします.