テストダブル作成
Mockeyのメインゴールは、テストダブル(代替物)の作成を手伝うことです。スタブやモック、スパイを作成できます。
スタブとモックは同じものが生成されます。2つの違いは、スタブが呼び出し時に指定した結果を返すだけなのに対し、モックは受け取るのを期待しているメソッド呼び出しのエクスペクションを指定できる必要があります。
スパイは受け取った呼び出しの記録を保持し、後ほど呼び出し結果を調査できるようにしてくれるテストダブルのタイプです。
テストダブルオブジェクトの作成時、テストダブルの名前として識別子を渡すことができます。識別子を渡さないと、そのテストダブルの名前は不明(unknown)となります。さらに、識別子はクラス名である必要はありません。グッドプラクティスであり、私達がおすすめしているのは、常にテストダブルの対象のクラスと同じ名前を付けることです。
テストダブルに存在しているクラスと同じ名前を使用すると、そのテストダブルは(継承により)そのクラスのタイプを継承します。たとえば、モックオブジェクトは存在しているクラスのタイプヒントや、instanceof
の評価を渡します。これは私達のコードが持つ期待を満足させるために、テストダブルが特定のタイプである必要がある場合に便利です。
スタブとモック
スタブとモックは、\Mockery::mock()
メソッドを呼び出し、生成します。以下の例では、"foo"という名前のスタブ、もしくはモックをどのように作成するかを示しています。
$mock = \Mockery::mock('foo');
このように生成されたモックオブジェクトは、最もゆるい形態のモックで、\Mockery\MockInterface
のインスタンスです。
Note: スタブ、モック、スパイにかかわらず、Mockeryにより生成されるテストダブルは、
\Mockery\MockInterface
のインスタンスです。
名前を持たないスタブやモックを生成するには、引数を付けずにmock()
を呼び出してください。
$mock = \Mockery::mock();
以前説明したように、無名のモックオブジェクトを生成することは推奨していません。
クラス、抽象クラス、インターフェイス
スタブやモックオブジェクトを生成するために推奨するのは、テストダブルを作成する対象の実クラス名を使用する方法です。
$mock = \Mockery::mock('MyClass');
このスタブ/モックオブジェクトは、継承によりMyClass
のタイプを保ちます。
スタブとモックオブジェクトは、具象クラス、抽象クラス、そしてインターフェイスでさえもベースにできます。一番の目的はタイプヒントに合致させるために、特定のタイプを確実にモックオブジェクトへ継承させることです。
$mock = \Mockery::mock('MyInterface');
このスタブ/モックオブジェクトは、MyInterface
インターフェイスを実装します。
Note: finalを指定したクラス、もしくはfinalを指定したメソッドを持つクラスは、完全にモックすることはできません。こうしたクラスに対して、Mockeryはパーシャル(部分)モックの作成をサポートしています。パーシャルモックについては、ドキュメントで後ほど解説します。
Mockeryは、一つのクラスで複数のインターフェイスを実装するクラスに基づいたスタブ/モックの生成もサポートしています。\Mockery::mock()
メソッドの最初の引数として、クラスとインターフェイスをコンマ区切りのリストで指定してください。
$mock = \Mockery::mock('MyClass, MyInterface, OtherInterface');
これでこのスタブは、MyClass
のタイプを持ち、MyInterface
とOtherInterface
インターフェイスを実装します。
Note: リストの最初の項目であるクラス名は必須ではありませんが、指定したほうが読みやすくフレンドリーでしょう。
モックへインターフェイスのリストを第2引数として渡し、実装することもできます。
$mock = \Mockery::mock('MyClass', 'MyInterface, OtherInterface');
これは直前の例と全く同じ、意図と目的です。
スパイ
Mockeryがサポートする、テストダブルの3つ目のタイプはスパイです。スパイとモックオブジェクトの主な違いは、スパイはテストダブルに対して行われた呼び出しの検査を呼び出し後に確認できることです。スパイはオブジェクトに対して行われる呼び出しの全てを確認する必要はない場合に使用します。
スパイは受け取ったメソッド呼び出し全てに対して、null
を返します。メソッド実行の戻り値をスパイに指定することはできません。そうしたいのであれば、スパイではなくモックオブジェクトを使用してください。
スパイを作成するには、\Mockery::spy()
メソッドを呼び出します。
$spy = \Mockery::spy('MyClass');
スタブ/モックと同様に、具象クラス、抽象クラスをベースにするか、好きな数のインターフェイスを実装するように、Mockeryへ指示できます。
$spy = \Mockery::spy('MyClass, MyInterface, OtherInterface');
このスパイは、これによりMyClass
のタイプで、MyInterface
とOtherInterface
を実装しました。
Note:
\Mockery::spy()
メソッドコールは実際には、\Mockery::mock()->shouldIgnoreMissing()
呼び出しの短縮形です。shouldIgnoreMissing
メソッドは、「振る舞いのモディファイヤ(変更指示)」です。すぐ後に説明します。
モック vs. スパイ
以下の例を使用し、モックとスパイの違いを説明しましょう。
$mock = \Mockery::mock('MyClass');
$spy = \Mockery::spy('MyClass');
$mock->shouldReceive('foo')->andReturn(42);
$mockResult = $mock->foo();
$spyResult = $spy->foo();
$spy->shouldHaveReceived()->foo();
var_dump($mockResult); // int(42)
var_dump($spyResult); // null
この例でわかるように、モックオブジェクトでは呼び出す前に呼び出しのエクスペクションを指定しており、返ってくることを期待している結果を得ています。一方のスパイオブジェクトは、実行された呼び出しを後ほど確認しています。スパイに対するメソッドコールは常にnull
を返します。
スパイ専用の章もご覧ください。
パーシャルテストダブル
パーシャル(部分)ダブルは、スタブのメソッドにエクスペクションを指定する、もしくはクラスの*いくつか*のメソッドをスパイするが、他のメソッドに関しては実際のコードを実行したい場合に便利です。
パーシャルテストダブルは、3つに分けています。
- ランタイムパーシャルテストダブル
- 生成パーシャルテストダブル
- プロキシパーシャルテストダブル
ランタイムパーシャルテストダブル
ランタイムパーシャルと呼ぶ場合、テストダブルを作成し、それからそれをパーシャルに指定することを意味します。許可(allow)か期待(expect)するようにダブルへ指示していないメソッド呼び出しは、全てオブジェクトの通常のインスタンス上で実行されます。
class Foo {
function foo() { return 123; }
function bar() { return $this->foo(); }
}
$foo = mock(Foo::class)->makePartial();
$foo->foo(); // int(123);
他のMockeryダブルと同様に、このテストダブルに呼び出しの許可や期待を指定できます。
$foo->shouldReceive('foo')->andReturn(456);
$foo->bar(); // int(456)
ランタイムパーシャルテストダブルの使用例は、クックブックの大きなParentクラスページで確認してください。
生成パーシャルテストダブル
生成可能な2つ目のパーシャルダブルタイプは、生成パーシャルと呼んでおり、Mockeryにどのメソッドが許可/期待できるのかを指定したものです。指定外のメソッドでは、 直接 実際のコードが実行されるため、スタブやメソッドに対するエクスペクションは動作しません。
class Foo {
function foo() { return 123; }
function bar() { return $this->foo(); }
}
$foo = mock("Foo[foo]");
$foo->foo(); // エラー、エクスペクションを指定していない
$foo->shouldReceive('foo')->andReturn(456);
$foo->foo(); // int(456)
// setting an expectation for this has no effect
$foo->shouldReceive('bar')->andReturn(999);
$foo->bar(); // int(456)
Note: 生成パーシャルテストダブルをサポートしていますが、この機能の使用は推奨していません。
理由の一つは、生成パーシャルはモックしようとするオリジナルクラスのコンストラクターを呼び出してしまうからです。これにより、アプリケーションコードをテストするとき、思いもしない副作用が生まれることでしょう。
詳細については、オリジナルのコンストラクターを呼び出さないを参照してください。
プロキシパーシャルテストダブル
パーシャルの最後の種類は、プロキシパーシャルモックです。finalが指定されているためモックにできないクラスに遭遇することがあります。似たようなケースで、finalが指定されているメソッドを持つクラスに出会うことがあります。このようなシナリオではモックするため、シンプルにクラスを拡張することも、メソッドをオーバーライドすることもできません。クリエイティブに解決する必要があります。
$mock = \Mockery::mock(new MyClass);
そうです、この新しいモックはプロキシです。呼び出しを横取りし、エクスペクションが指定されていないメソッド呼び出しは、(生成し、mockメソッドに渡した)仲介するオブジェクトへ渡し直します。これにより間接的にfinalのメソッドをモックできます。なぜなら、プロキシは制約を受けないからです。トレードオフは明確です。プロキシパーシャルはモックしようとしているクラスのタイプヒントのチェックに失敗します。なぜなら、そのクラスを拡張できないからです。
エイリアス
(まだロードされていない)クラスの名前に、"alias:"をプレフィックスとして付けると、「エイリアスモック」が生成されます。エイリアスモックは、指定したクラス名でstdClassのクラスエイリアスを作成します。このエイリアスはpublicの静的メソッドをモックできるようにするために使用します。新しいモックオブジェクトへ指定される、静的メソッドを参照するエクスペクションは、このクラスへの全静的呼び出しにより使用されます。
$mock = \Mockery::mock('alias:MyClass');
Note: クラスのエイリアスをサポートしていますが、推奨していません。
オーバーロード
(まだロードされていない)有効なクラス名へ"overload:"をプリフィックスとして付けると、("alias:"と同様に)エイリアスモックを生成します。違いは、そのクラスの新しいインスタンスが生成され、オリジナルのモック($mock
)に指定されたエクスペクションを全てインポートすることです。オリジナルのモックは新しいインスタンスへエクスペクションを保存するために使用されるため、検査されることはありません。シンプルな「エイリアスモック」と区別するために、「インスタンスモック」という言葉を使用しています。
言い換えれば、モックしたクラスの新しいインスタンスが生成されて時に、インスタンスモックは「横取り」し、モックが代わりに使用されます。これは特に、後ほど説明する依存が強い場合のモックで便利です。
$mock = \Mockery::mock('overload:MyClass');
Note: 2つ以上のテスト間で、エイリアス/インスタンスモックを使用すると、同じ名前の2つのクラスは持てないため、fatalエラーが発生します。これを防ぐには、この種のテストは、独立したPHPプロセスで実行してください。PHPUnitとPHPTで、サポートされています。
名前付きモック
namedMock()
は最初の引数により呼び出されるクラスを生成します。下記の例の場合は、MyClassName
です。残りの引数は、mock
メソッドと同じ取り扱いです。
$mock = \Mockery::namedMock('MyClassName', 'DateTime');
この例では、DateTime
を拡張した、MyClassName
という名前のクラスが生成されます。
名前付きモックが使用されるのは極めてまれですが、コードが__CLASS__
マジック定数に依存しているか、一つの抽象クラスから派生した、実際には別の2つのクラスが必要な場合に役立ちます。
名前付きモックの使用例は、クックブックの章のクラス定数をご覧ください。
Note: 名前付きモックは一度のみ生成でき、以降に別の引数で
namedMock
を呼び出すと例外が発生します。
コンストラクターの引数
モックするクラスが、コンストラクタ引数を必要とする場合が時々あります。2つ目の引数にインデックスされた配列としてMockeryへ渡すことができます。
$mock = \Mockery::mock('MyClass', [$constructorArg1, $constructorArg2]);
もし、同時にMyClass
がインターフェイスを実装する場合は、第3引数として渡します。
$mock = \Mockery::mock('MyClass', 'MyInterface', [$constructorArg1, $constructorArg2]);
これで、Mockeryは$constructorArg1
と$constructorArg2
をコンストラクターへ渡すことがわかります。
振る舞いモディファイヤー
モックオブジェクトを作成する際、Mockeryのデフォルトの振る舞いではなく、一般的によく使用される振る舞いにしたい場合があります。
shouldIgnoreMissing()
振る舞いモディファイヤーを使用すると、このモックはパッシブモックであるとラベル付けることができます。
\Mockery::mock('MyClass')->shouldIgnoreMissing();
このようなモックオブジェクトでは、エクスペクションでカバーされていないメソッドの呼び出し時に、一致するエクスペクションが見つからない通常のエラーの代わりに、null
を返します。
PHP7.0.0以降では、エクスペクションが見つからず、返すタイプを持つメソッドは、(返すタイプがクラスの場合)オブジェクトのモックか、空文字列や空配列、整数や実数ではゼロ、論理値ではfalse、もしくは空のクロージャのような「偽」にあたるプリミティブな値のどれかを返します。
PHP7.1.0以降では、エクスペクションが見つからないメソッドで、返すタイプがnullableの場合,nullを返します。
(たとえば、null
オブジェクトの)\Mockery\Undefined
タイプのオブジェクトを返したい場合(バージョン0.7.2での振る舞い)は、追加のモディファイヤーを使用することもできます。
\Mockery::mock('MyClass')->shouldIgnoreMissing()->asUndefined();
返されるオブジェクトはプレースホルダー(代用品)でしかないため、使うべきでない場所で使用する間違った使い方を行えば、運命の導きにより、ロジックチェックをパスしないでしょう。
makePartial()
メソッドは、ランタイムパーシャルテストダブルを生成するメソッドとして、既に説明しました。
\Mockery::mock('MyClass')->makePartial();
この形式のモックオブジェクトは、エクスペクションの対象外のメソッドをモックの親クラス、この例の場合はMyClass
へ引き渡します。前のshouldIgnoreMissing()
がnull
を返していましたが、この振る舞いは単に一致する親のメソッドを呼び出すだけです。