Mockery 0.8.0 日本語ドキュメント
追記:この翻訳の内容はだいぶ古いため、2018年2月3日現在の安定バージョンである、1.0.0のドキュメントを翻訳しました。
https://readouble.com/mockery/1.0/ja/index.html
Laravel5.5LTSでサポートしているのも1.0.0ですので、上記サイトの翻訳を参照してください。
Mockery
Mockeryはシンプルですが、柔軟なPHPモックオブジェクトフレームワークです。PHPUnitやPHPSpec、もしくは他のテストフレームワークと共に、ユニットテストで使用します。主な目的は、簡潔なAPIのテストダブルフレームワークを提供することです。可能性のある全てのオブジェクト操作とやり取りを明確に定義でき、人間が読むことができるドメイン固有言語を使用しています。PHPUnitのphpunit-mock-objectsライブラリーの代わりとして使用できるように設計されています。MockeryはPHPUnitと簡単に統合でき、大きな手間をかけずにphpunit-mock-objectsライブラリーと一緒に動かすことができます。
Mockeryは新BSDライセンスでリリースしています。
PEARの最新バージョンは0.8.0です。Composerユーザーであれば0.8.0という固定的なgitタグを使用する代わりに現在のマスターブランチを使用するオプションを指定できます。現在のマスターブランチはTravis CIでトラッキングできます。
モックオブジェクト
ユニットテストではモックオブジェクトが、本物のオブジェクトの振る舞いをシミュレートします。テストを独立させるため、もしくはまだ存在していないオブジェクトの代理をさせるため、実装され提供する必要のあるクラスAPIの設計を実際に確認するために、よく使用されます。
モックオブジェクトフレームワークの利点は、このようなモックオブジェクト(とスタブ)を柔軟に生成できることです。自然言語での記述にできるだけ近づけた記法を使い、本当のオブジェクトの可能性のある振る舞い全部を捕らえることができる柔軟なAPIを用いることで、予期されるメソッドの呼び出しを設定し、返される値を設定することが可能になります。
動作要件
MockeryはPHP 5.3.2以上で動作します。さらにメソッドの引数の期待値条件を定義する場合に利用できる追加のテスト条件を含んだ、Hamcrestライブラリーのインストールも推奨します。(以降のインストラクションをご覧ください。)
インストール
MockeryはComposerかPEAR、もしくはGitHubのレポジトリーをクローンすることでインストールできます。以降では、この3つの選択肢を概説します。
Composer
Composerとメインのリポジトリーについて詳しく知りたい場合はhttp::/packagist.orgをご覧ください。Composerを使い、Mockeryをインストールするには、最初にPackagistホームページのインストラクションに従い、Composerを先にインストールします。それから以下で指定している推奨するパラメーターを指定し、Mockeryの開発依存パッケージを定義します。その時に安定版マスターブランチに保つため、現在の安定バージョンのタグを代わりに指定することもできます。
{ "require-dev": { "mockery/mockery": "dev-master@dev" } }
続いて以下のコマンド実行でインストールされます。
composer.phar install --dev
これで通常の実用運用向けパッケージ依存指定(--dev無し)ではMackeryはインストールされず、開発向けオプション(--dev)を指定した時のみインストールされるようになります。(訳注:Composerのバージョンや設定により、インストールされる可能性はあります。最新バージョンでは--devがデフォルトです。開発パッケージのインストールを明示的に避ける場合は、--no-devオプションを指定します。)
PEAR
Mockeryはsurvivethedeepend.com PEARチャンネルにホストされており、以下のコマンドでインストールできます。
sudo pear channel-discover pear.survivethedeepend.com sudo pear channel-discover hamcrest.googlecode.com/svn/pear sudo pear install --alldeps deepend/Mockery
Git / GitHub
このリポジトリーでは開発バージョンをマスターブランチとしてホストしています。前の例で示した通り、dev-masterとしてプロジェクト中のcomposer.jsonとして指定することでComposerにこれをインストールするように指定できます。同様にPEARを使用してもこの開発バージョンをインストールできます。
git clone git://github.com/padraic/mockery.git cd mockery sudo pear channel-discover hamcrest.googlecode.com/svn/pear sudo pear install --alldeps package.xml
上記の手順によりMockeryとHamcrest両方がインストールされます。HamcrestをインストールしなくてもMockeryは動作しますが、引数の指定に使用する、よりバラエティに満ちた期待値条件指定の機能を追加するためインストールを推奨します。
ユニットテスト
Mockeryのユニットテストを実行するためには、gitリポジトリーをクローンし、http://getcomposer.org/download/の手順に従いComposer(すなわちcomposer.phar)をダウンロードします。続いてMockeryのルートディレクトリーで以下のComposerコマンドを実行してください。
php /path/to/composer.phar install --dev
これによりHamcrest開発依存パッケージをインストールし、ユニットテストに必要なオートロードファイルが生成されます。"tests"ディレクトリーに移動し、通常通りphpunitコマンドを実行してください。多少でも運をお持ちなら、失敗せずテストが完了するでしょう!
0.8.*へのアップデート
0.8.0以降のリリースでは以下の動作に注意してください。
shouldIgnoreMissing()はモックオブジェクトに対し、期待されている期待値条件と合わないメソッドの呼び出しが行われた場合に、\Mockery\Undefinedのインスタンスをリターンするように動作していました。0.8.0から、代わりにNULLがリターンされるように変更されました。0.7.2の動作と同様に実行するには、以下のように指定してください。
$mock = \Mockery::mock('stdClass')->shouldIgnoreMissing()->asUndefined();
シンプルなサンプル
平均気温をレポートするために、ある地方の気温を測定するTemperatureクラスがあるとイメージしてください。そのデーターはWebサービスから取得しても、他の情報源から取得してもかまいません。ですが現在手元にありません。しかしTemperatureクラスとのやり取りに基づき、それらのクラスの基本的なインターフェイスを仮定することはできます。
class Temperature { public function __construct($service) { $this->_service = $service; } public function average() { $total = 0; for ($i=0;$i<3;$i++) { $total += $this->_service->readTemp(); } return $total/3; } }
実際のサービスクラスが無くても、どのような操作がされるかことが期待されるのか理解できます。Temperatureクラスに対するテストを書くには、実際に必要な具象サービスのインスタンスが存在しなくても、振る舞いをテストするため、本物のサービスをモックオブジェクトで今のところ置き換えておくことは可能です。
注目:PHPUnitと統合すれば、tearDown()メソッドは必要ありません。(後述)
use \Mockery as m; class TemperatureTest extends PHPUnit_Framework_TestCase { public function tearDown() { m::close(); } public function testGetsAverageTemperatureFromThreeServiceReadings() { $service = m::mock('service'); $service->shouldReceive('readTemp')->times(3)->andReturn(10, 12, 14); $temperature = new Temperature($service); $this->assertEquals(12, $temperature->average()); } }
APIの詳細については後述します。
PHPUnit統合
Mockeryはスタンドアローンなモックオブジェクトフレームワークとして、シンプルに使用できるよう設計されています。ですから、どんなテストフレームワークと統合し使用することもできます。Mockeryを統合するには、テストのtearDown()メソッドに以下のコードを含めてください。(\Mockery名前空間をエイリアスで短縮して使用することもできます。)
public function tearDown() { \Mockery::close(); }
このスタティックな呼び出しは現在のテストで使用されたMockeryコンテナをクリーンアップし、他の期待値条件の確認に必要な作業を続けて実行できるようにします。
Mockeryをより簡単に使用できるようにするため、Mockery名前空間の指定を短縮名で指定することもできます。例えば:
use \Mockery as m; class SimpleTest extends PHPUnit_Framework_TestCase { public function testSimpleMock() { $mock = m::mock('simple mock'); $mock->shouldReceive('foo')->with(5, m::any())->once()->andReturn(10); $this->assertEquals(10, $mock->foo(5)); } public function tearDown() { m::close(); } }
テストにrequire_once()の呼び出しを書かなくても済むように、Mockeryにはオートローダーが同包されています。これを使用するにはMockeryがinclude_pathに含まれていることを確認し、以下のコードをテストスーツのBootstrap.phpかTestHelper.phpファイルに含めてください。
require_once 'Mockery/Loader.php'; require_once 'Hamcrest/Hamcrest.php'; $loader = new \Mockery\Loader; $loader->register();
Composerをご利用でしたら、Composerが生成したオートローダーファイルをシンプルにincludeするだけですみます。
require __DIR__ . '/../vendor/autoload.php'; // vendorディレクトリがひとつ上の階層であると仮定
(注意:Hamcrest 1.0.0以前ではHamcrest.phpファイル名は小文字のhでした。すなわちhamcrest.phpです。Hamcrest 1.0.0にアップグレードした場合、全プロジェクトでファイル名を更新しているか、チェックするのをお忘れなく。)
MockeryをPHPUnitと統合し、closeメソッドの呼び出しを省略し、さらにコードカバレージレポートからMockeryを除外するには、以下のコードを適用してください。
// テストスーツの作成 $suite = new PHPUnit_Framework_TestSuite(); // テスト結果リスナーを生成し追加 $result = new PHPUnit_Framework_TestResult(); $result->addListener(new \Mockery\Adapter\Phpunit\TestListener()); // テストの実行 $suite->run($result);
PHPUnitのXMLによる設定アプローチを使用している場合、TestListenerをロードするために以下の指定が使用できます。
<listeners> <listener class="\Mockery\Adapter\Phpunit\TestListener"></listener> </listeners>
ComposerかMockeryのオートローダーがbootstrapファイル中に存在していること、もしくは上記のTestListenerクラスのファイルが"file"属性で指定されていることを確認してください。
警告:PHPunitのテストを独立したプロセスで実行する
PHPUnitはテストを独立したプロセスとして実行する機能を提供しています。Mockeryはモックの期待値条件をMockery::close
メソッドが呼び出されることで検証し、その結果を全てのテストの後に自動的にこのメソッドを呼び出す、PHPUnitリスナーに提供します。
しかしながら、このリスナーはPHPUnitのプロセスを独立して実行する場合、正しいプロセスにより呼びだされません。関連がない期待値条件の結果が返され、Mockery\Exception
も投げられません。これを防ぐには、Mockery PHPUnit TestListener
を信頼せずに、以前に説明してある通り、tearDown()
メソッドの呼び出しの中で、明確にMockery::close
を呼び出す必要があります。
クイックリファレンス
Mockeryはモックを作成するために簡潔なAPIを実装しています。最初は次のようなメソッドの呼び出しになるでしょう。。
$mock = \Mockery::mock('foo');
fooという名前のモックオブジェクトを生成しています。この場合、fooは例外が発生する場合に識別のために使用されるだけの名前です。(クラス名である必要はありません。)これで\Mockery\Mockモックオブジェクトが生成されます。一番簡単なモックです。
$mock = \Mockery::mock(array('foo'=>1,'bar'=>2));
名前が指定されていませんので、unknownという名前のモックオブジェクトが生成されます。ですが、期待値条件の配列を渡しています。メソッドと、それがリターンする期待値条件をセットするための、手軽な指定方法です。
$mock = \Mockery::mock('foo', array('foo'=>1,'bar'=>2));
前の例と似ていますが、前述の例を2つとも使用しています。mock()の2番めのパラメーターで指定した通りに、全モックオブジェクトに期待値条件の配列を渡すことができます。
$mock = \Mockery::mock('foo', function($mock) { $mock->shouldReceive(method_name); });
期待値条件の配列に追加するため、再利用可能な期待値条件を含んだクロージャーを渡すこともできます。2番めのパラメーター、もしくは期待値条件の配列と一緒に指定する場合は3つ目のパラメーターとして渡します。これは再利用可能なモックの期待値を生成する一つの方法です。
$mock = \Mockery::mock('stdClass');
名前が実際のクラス名であることを除いては、名前付きのモックと同じモックを生成します。今までの例で見てきたようにシンプルなモックを生成しますが、違いはこのモックオブジェクトはクラスタイプを継承することです。すなわちタイプヒントやstdClassへのinstanceofの評価が渡されます。タイプを指定してモックオブジェクトを使用する場合に便利です。
$mock = \Mockery::mock('FooInterface');
どんな具象タイプ、abstractクラス、さらにインターフェイスをも元にしてモックオブジェクトを生成できます。再度確認しますが、主な目的はタイプヒンティングのために、モックオブジェクトへ特定のタイプを確実に継承させるための機能です。例外はクラスがfinalである場合と、メソッドにfinalが指定されている場合で、完全なモックとして動作しません。このような場合、後述のパーシャル(部分)モックを使用してください。
$mock = \Mockery::mock('alias:MyNamespace\MyClass');
まだロードされていない有効なクラス名の前に"alias:"をプレフィックスとしてつけると、「エイリアスモック」を生成します。エイリアスモックは指定されたクラス名でstdClassのクラスエイリアスを生成します。一般的にはpublic staticメソッドをモックできるように使用します。スタティックなメソッドを参照する新しいモックオブジェクトにセットした期待値条件は、このクラスに対するスタティックな呼び出し全部により使用されます。
$mock = \Mockery::mock('overload:MyNamespace\MyClass');
現在ロードされていない有効なクラス名に"overload:"のプレフィックスをつけると"alias:"と同様に、エイリアスモックを生成します。オリジナルのモック($mock)にセットされた期待値を取りこんだ、クラスの新しいインスタンスを生成する違いがあります。新しいインスタンスに保存される期待値を使用する場合でも、オリジナルモックは全く確認されません。「インスタンスモック」という言葉を使う目的は、シンプルな「エイリアスモック」との区別をつけるためです。
注意:同じエイリアス/インスタンスモックを一つ以上のテストで使用すると、同じ名前で複数のクラスは持てないため、Fatalエラーが発生します。これを避けるには、この種のテストはそれぞれ別のPHPプロセスで実行してください。PHPUnitやPHPTの両方でサポートされています。
$mock = \Mockery::mock('stdClass, MyInterface1, MyInterface2');
最初の引数はそのモックオブジェクトで実装する必要があるインターフェイスのリストも受け付けます。オプションとして一つだけ存在するベースクラスを含めることができます。このリストの最初のメンバーは必要がありませんが、可読性を上げるためにつけています。これに引き続く残りの引数は、以前の例と変わりありません。
指定されたクラスが存在しない場合、それを定義し、前もってincludeしててください。そうでない場合は\Mockery\Exceptionが投げられます。
$mock = \Mockery::mock('MyNamespace\MyClass[foo,bar]');
上記の文法はMockeryにfoo()とbar()メソッドだけをモックするMyNamespace\MyClassクラスのパーシャルモックを指示しています。その他のメソッドはMockeryでオーバーライトされません。この従来の「パーシャルモック」の形態はどんなクラスやabstractクラスにも適用できます。例えば具体的な実装がされず、存在していないabstractメソッドをモックできます。finalに指定されたメソッドをパーシャルモックしようとすると、そのインスタンスのfinalメソッドは何も手を加えず無視されます。PHPの定義により、finalメソッドをモックするのは不可能なためです。
Mockeryでパーシャルモックを作成する方法の詳細はパーシャルモックの生成を参照してください。
$mock = \Mockery::mock("MyNamespace\MyClass[foo]", array($arg1, $arg2));
2つ目か3つ目の引数にインデックス付きの配列があれば、Mockeryはコンストラクタに対するパラメーターとして仮定し、モックオブジェクトのコンストラクト時にモックオブジェクトへ渡します。上記の指定では新しいパーシャルモックが生成され、bar
メソッドが内部的に$this->foo()
でfoo
メソッドを呼び出している場合、特に便利です。
$mock = \Mockery::mock(new Foo);
実在するオブジェクトをMockeryに渡すと、プロキシ(代理)されたパーシャルモックを生成します。これは特に本当のパーシャルが利用できない場合に便利です。例えば、finalクラスやfinel付きのメソッドを必ずオーバーライドしなくてはならないようなクラスです。実オブジェクトを既に生成しているのですから、期待値条件のために、存在しているメソッドのサブセットを選択的にオーバーライド、もしくは存在しないメソッドを付け加えることも、必要に応じ全て行えます。
小さな改訂:全てのモックメソッドは最初の引数としてクラス、オブジェクト、エイリアス名を受け付けます。2つ目の引数にはメソッドの期待値の配列とリターン値、もしくは期待値のクロージャー(期待値の配列と一緒に使用する場合は3つ目)を指定します。
\Mockery::self()
時々、モックが持っているメソッドが同じモックオブジェクトを期待値としてリターンする必要があることに気がつくでしょう。例えばよくあるケースとしては、Mokeryが自分自身をリターンするように、ドメイン特定言語(DSL)を設計している場合です!これを楽に行えるように、\Mockery::self()の呼び出しは、\Mockery::mock()により生成されたモックオブジェクトの最新をいつでもリターンします。
$mock = \Mockery::mock('BazIterator') ->shouldReceive('next') ->andReturn(\Mockery::self()) ->mock();
上のモックされているクラスはnext()メソッドが示しているようにiteratorです。多くの場合、個々の反復要素として動作させるようにプログラムした、単一のモックオブジェクトを用いて、(同じタイプを何度も操作するのですから)全ての反復要素を置き換えることができます。
振る舞いの変更指示
モックオブジェクトを生成する場合、Mockeryのデフォルト動作ではない、お望みの振る舞いを使用したい場合があります。
\Mockery:mock('MyClass')->shouldIgnoreMissing()
shouldIgnoreMissing()振る舞い変更メソッドを使用すれば、このモックオブジェクトはパッシブモックであるとラベルを付けます。このモックオブジェクトは一致した呼び出しに対する期待値条件がないことを通知する通常の処理の代わりに、期待値条件でカバーされないメソッドの呼び出し時にはNULLをリターンします。
お好みであれば追加の振る舞い変更指定を使用し、(0.7.2での動作である)\Mockery\Undefinedタイプのオブジェクト(すなわちnullオブジェクト)をリターンすることもできます。
\Mockery:mock('MyClass')->shouldIgnoreMissing()->asUndefined()
リターンオブジェクトはプレースホルダーにしか過ぎません。ですから運命のいたずらで、不適切な箇所で間違って指定してしまうとたぶんロジックチェックを通らないでしょう。
\Mockery::mock('MyClass')->makePartial()
もしくは
\Mockery::mock('MyClass')->shouldDeferMissing()
パッシブパーシャルモックとして認識されているモック(後ほど説明する本当のパーシャルモックと間違えないでください)、この形態のモックオブジェクトは期待値条件を設定していない全てのメソッドの処理を親のクラスのモックに譲ります。すなわちこの場合はMyClassです。
期待値条件の宣言
モックオブジェクトを生成したら、通常、実際どう振る舞うべきかの定義を始めたいと思うことでしょう。(それとどう呼び出されるかもです。)これはMockeryの期待値条件で行います。
shouldReceive(method_name)
モックで指定されたメソッド名が呼び出されることを宣言します。これは他の全ての期待値条件の初めに指定され、制約が追加されます。
shouldReceive(method1, method2, ...)
複数の呼び出しが期待されるメソッドを宣言します。全手のメソッドに対し、続けてチェーンされた期待値条件もしくは制約が適用されます。
shouldReceive(array('method1'=>1, 'method2'=>2, ...))
複数の呼び出しだけでなく、戻り値も宣言します。全てに対しチェーンで追加された期待値条件と制約が適用されます。
shouldReceive(closure)
(パーシャルモックからのみ)モックオブジェクトを生成します。モックオブジェクトレコーダーを生成するために使用されます。レコーダーはモックするためにオリジナルのオブジェクトへ渡す目的のシンプルなプロキシです。これはクロージャーへ渡され、実行される一連の操作をパーシャルモックへ期待値条件としてとして記録します。シンプルな使用例は存在している使用状況に基づいて、自動的に期待値を記録する場合です。(例えばリファクタリングの最中)後のセクションのサンプルをご覧ください。
with(arg1, arg2, ...)
メソッドが呼び出された場合に引数がリストの値に一致することを期待する制約を付け加えます。組み込みマッチングクラスを使用し、柔軟にもっと多くの引数を追加することも可能です。(後述)例えば\Mockery::any()はwith()の引数として指定し、その位置の引数には何を渡されてもパスさせるためのマッチングクラスです。MockeryではHamcrestライブラリーのマッチングクラスも指定可能です。例えばHamcrest関数のanything()は\Mockery::any()と同じ働きをします。
ここで重要なポイントは、指定されたメソッドに対して完全に一致する引数の呼び出しに対してのみ、付加された全期待値条件が適用されることです。これにより呼び出し時の引数に基づき、異なった期待値を指定できます。
withAnyArgs()
この期待値条件はメソッドの呼び出しでどんな引数が渡されてもパスすることを宣言します。これは他の期待値条件が指定されていない場合のデフォルト値です。
withNoArgs()
この期待値条件は呼び出し時に引数が無いことを宣言します。
andReturn(value)
メソッド呼び出しからの戻り値を指定します。
andReturn(value1, value2, ...)
一連の戻り値、もしくはクロージャーを指定します。例えば最初の呼び出しではvalue1が、二回目はvalue2がリターンされます。それ以降の全てのモックされたメソッドの呼び出しに対しては最後の値がいつもリターンされることに注意してください。
andReturnNull() / andReturn([NULL])
上記2つのオプションは主にテストレコーダーとのコミュニケーションのために使用されます。モックオブジェクトがNULLか何も返さないことを示します。
andReturnValues(array)
andReturn()の別の記述法でパラメーターのリストの代わりに単純な配列を受け取ります。リターンされる順番は配列のインデックスの数字で決まり、要素数以上の呼び出しがあった場合は最後の項目がリターンされます。
andReturnUsing(closure, ...)
メソッドに渡された引数が渡されるクロージャー(無名関数)を指定します。クロージャーからの戻り値がリターンされます。動的に引数を処理し、関連する実際の結果をリターンしたい場合に便利です。andReturn()と同じように渡された順番で使用され、引数の数以上の呼び出し回数の場合も同様に処理されます。現在andReturnUsing()をandReturn()と一緒に使用することはできませんので注意してください。
andThrow(Exception)
このメソッドを呼び出した時に指定された例外が投げられることを宣言します。.
andThrow(exception_name, message)
モックされたメソッドから例外を投げる場合、オブジェクトより、例外クラスとメッセージを渡すほうが使いやすいでしょう。
andSet(name, value1) / set(name, value1)
他の期待値条件と共に使用し、一致するメソッドが呼び出された時に、モックオブジェクトのpublicプロパティに指定された値をセットするために使用します。
passthru()
この期待値条件は戻り値として指定したキューを使用せず、代わりにモックしようとしている対象のクラスの本当のメソッドを呼び出し、結果を返すように指示します。基本的に、実際のメソッドに対し期待値条件のマッチングと呼び出し回数のバリデーションは適用されますが、期待されている引数で本当のメソッドを呼び出します。
zeroOrMoreTimes()
メソッドが0回以上呼び出されることを宣言します。他の指定がない場合、これがデフォルトです。
once()
メソッドが一回だけ呼び出されることが期待されていると宣言します。他の実行回数束縛と同様に、束縛に違反した場合\Mockery\CountValidator\Exceptionが投げられます。さらにatLeast()とatMost()束縛で変更できます。
twice()
メソッドが2回だけ実行されることが期待されていると宣言します。
times(n)
メソッドがn回だけ実行されることが期待されていると宣言します。
never()
メソッドが呼び出されないことが期待されていることを宣言します。0回です!
atLeast()
最低でも次に指定する期待値回数実行する束縛を追加します。ですからThus atLeast()->times(3)は(メソッドの引数が一致する)最低でも3回の呼び出しが行われなくてはならず、より少なくてはいけません。
atMost()
実行回数の最大実行回数の束縛を追加します。ですからatMost()->times(3)は実行は3回以下で無くてはなりません。これは全く呼び出されない場合もパスします。
between(min, max)
呼び出し回数の範囲を指定します。これはatLeast()->times(min)->atMost()->times(max)と同じ働きをしますが、短く記述できます。APIの自然言語としての読みやすさを考慮し、パラメーターなしのtimes()に続けて指定することもできるようになっています。
ordered()
このメソッドが同種の宣言をしたメソッドの中で特定の順番で実行されることを宣言します。実行順序はモックのセットアップで実際にこの期待値条件を宣言した順番で決まります。
ordered(group)
メソッドが順序グループに所属していることを宣言します。グループは名前でも数字でもかまいません。同じグループに所属しているメソッドはどの順番で呼び出しても構いませんが、グループ外から呼び出す場合はそのグループを指定した順番で呼びださなくてはなりません。例えばmethod1をgroup1のmethod2の前に実行するように指定できます。
globally()
これをordered()かordered(group)の前に指定すると、現在のモックの中だけでなく、全モックオブジェクトでの実行順序を指定できます。複数のモックにまたがって順番を指定したい場合に使用できます。
byDefault()
デフォルトとして指定します。デフォルト期待値はデフォルトではない期待値が使われるまで、適用されます。あとで定義されたデフォルト期待値は、先に宣言されたものを即座に置き換えます。これはユニットテストのsetup()でデフォルトモックを準備し、後ほど特定のテストで必要に応じ調整したい場合に便利です。
getMock()
期待値条件のチェーン中で現在のモックをリターンします。一文でセットアップしたモックを取っておきたい場合に便利です。例えば:
$mock = \Mockery::mock('foo')->shouldReceive('foo')->andReturn(1)->getMock();
引数のバリデーション
with()宣言に渡された引数は期待値をセットすることで、期待値条件に一致したメソッドを呼び出すための抽出条件となります。ですから、異なった引数の期待値を別々に指定することで、一つのメソッドに対したくさんの期待値をセットできます。この引数のマッチングは「最良適合」を基本として行われます。これにより明白な一致は汎用的な一致より確実に優先されます。
明確な一致とは単に期待している引数と実際の引数がイコールで(例えば===や==で一致する)あることを意味します。より汎用的な一致とは正規表現を使用するとか、クラスヒント、より包括的にマッチする可能性があるものを指します。汎用的なマッチングの目的は明確な言葉で定義しなくても引数を定義できるようにすることです。例えばMockery::any()をwith()に使用するとその位置の引数はどんなものでもマッチします。
Mockeryの汎用マッチングは全ての可能性をカバーしていませんが、Hamcrestライブラリーのマッチングを使用することもできます。Hamcrestは似た名前のJavaライブラリーを移植したものです。(PythonやErlangなどにも移植されています。)私はHamcrestを使用することを強くお勧めします。自然な英語のDSLとして自ら推奨している通り、Hamcrestの素晴らしいライブラリーが既に存在しているのですから、同じ機能をMockeryに取り込む必要は感じていないからです。
以下にMockeryのマッチングと、同じ働きをするHamcrestの機能を示します。Hamcrestは名前空間なしの関数を使用しています。
取り得る期待値のサンプルです。
with(1)
整数の1にマッチしますこれは===でテストされます。また引数に文字列の'1'を指定すれば、より厳密ではない==チェックを簡単に行えます。
with(\Mockery::any()) OR with(anything())
どの様な引数にもマッチします。基本的に何であろうと制約することなく、その場所の引数はパスされます。
with(\Mockery::type('resource')) OR with(resourceValue()) OR with(typeOf('resource'))
どんなリソースもマッチングします。例えばis_resource()呼び出しからtrueをリターンします。タイプマッチはどんな文字列も受け付け、"is_"をつけたタイプのバリデーションチェックを行います。例えば\Mockery::type('float')やHamcrestのfloatVaule()とtypeOf('float')はis_float()を使用しチェックを行います。\Mockery::type('callable')やHamcrestのcallable()はis_callable()を使用します。
また、クラスやインターフェイス名も受け付け、指定された引数をinstanceofで評価します。(HamcrestでanInstanceOf()を使うのと似ています。)
タイプチェッカーの完全なリストはhttp://www.php.net/manual/ja/ref.var.phpにあります。またHamcrestの関数リストはhttp://code.google.com/p/hamcrest/source/browse/trunk/hamcrest-php/hamcrest/Hamcrest.phpです。
with(\Mockery::on(closure))
Onマッチはクロージャー(無名関数)を受け付け、指定された引数を渡します。そのクロージャーがtureを評価したら(例えばreturn)、その引数は期待値に合っているとして扱いますこれは引数の期待値の指定が余りにも複雑になった場合や、シンプルに現在のデフォルトマッチを実装していない場合に有効です。
これはHamcrestの関数にはありません。
with('/^foo/') OR with(matchesPattern('/^foo/'))
この引数の定義は指定された文字列を正規表現として実際の引数とマッチングを行います。正規表現オプションは次の場合のみ使用されます。a)===や==でのマッチが存在しない。b)(preg_match()がfalseを返さない)有効な正規表現である。もし正規表現によるマッチングがあなたのテストにそぐわないのでしたら、Hamcrestがより明示的なmatchesPattern()関数を提供しています。
with(\Mockery::ducktype('foo', 'bar'))
Duchtypeマッチはクラスタイプによる別のマッチングです。呼び出す指定リスト上のメソッドを含んでいるオブジェクトであればどの様な引数ともマッチします。
これはHamcrestの関数にはありません。
with(\Mockery::mustBe(2)) OR with(identicalTo(2))
mustBeマッチはデフォルトの引数マッチよりも厳格です。デフォルトのマッチはPHPのタイプキャストを許しますが、mustBeマッチでは引数が期待値と同じタイプであることも確認します。つまりデフォルトでは引数で指定した'2'は実際の引数の2(整数)とマッチしますが、同じ状況でもmustBeではマッチしません。なぜなら文字列の引数が期待されていますが、代わりに整数が指定されているからです。
注意:PHPでは2つのオブジェクトが全く同じインスタンスでなければ不一致とみなされるため、このマッチを使いオブジェクトを比較し等しいとは仮定できません。これは以前に生成済みのオブジェクトがリターンされる場合に障害となります。なぜなら全く同じものであると比較するのは不可能だからです。
with(\Mockery::not(2)) OR with(not(2))
notマッチは引数と等しくないか同一でない場合にマッチします。
with(\Mockery::anyOf(1, 2)) OR with(anyOf(1,2))
指定された引数のどれかと等しい場合にマッチします。
with(\Mockery::notAnyOf(1, 2))
指定されたパラメーターのどれとも等しくない、もしくは同一ではない場合にマッチします。
これはHamcrestの関数にはありません。
with(\Mockery::subset(array(0=>'foo')))
指定された配列のサブセットを含んでいる配列にマッチします。キー値と値に対して比較されます。つまりそれぞれの実際の要素のキーと値が比較されます。
Hamcrest版の関数は存在しません。代わりにHamcrestは一つの要素をhasEntry()かhasKeyValuePair()でチェックできます。
with(\Mockery::contains(value1, value2))
リストした値を含んでいる配列にマッチします。キーは無視されます。
with(\Mockery::hasKey(key));
指定されたキー値を含んでいる配列にマッチします。
with(\Mockery::hasValue(value));
指定された値を含んでいる配列にマッチします。
パーシャルモックを作成する
パーシャルモックはあるオブジェクトのいくつかのメソッドだけをモックし、残りは通常の呼び出しのままにする必要がある時に便利です。(例えば実装済みのメソッドなど。)Mockeryにはパーシャルモックを作成する3つの手法があります。それぞれ利点と欠点を持っており、どれを利用するかはあなたの好みとモックする必要のあるソースコードにより決まります。
- 従来のパーシャルモック
- パッシブ(受動的)パーシャルモック
- プロキシ(代理)パーシャルモック
従来のパーシャルモック
従来のパーシャルモックは、まず先にモックするクラスのメソッドを処理し、モックしないもの(例えば普通に呼び出せるメソッド)はそのままにしておく方法と定義できます。従来のモックは次のように記述し作成します。
$mock = \Mockery::mock('MyClass[foo,bar]');
上記の例ではMyClassのfoo()とbar()メソッドがモックされ、残りのメソッドは何も変更されません。たぶんfoo()とbar()メソッドにモックとしての動作を指示するため、期待値条件を定義する必要があるでしょう。
モックしないメソッドが頼りにしているでしょうから、コンストラクタの引数を渡せることを忘れないでください。
$mock = \Mockery::mock("MyNamespace\MyClass[foo]", array($arg1, $arg2));
パッシブパーシャルモック
パッシブパーシャルモックはデフォルトの状態にモックを付け加えたものです。
$mock = \Mockery::mock('MyClass')->makePartial();
パッシブパーシャルでは、メソッドの呼び出しが期待値条件に一致しない限り、全てのメソッドをシンプルに親のクラス(MyClass)のオリジナルメソッドに引き渡します。特定のメソッド呼び出しに対して一致する期待値条件が無い場合、その呼び出しはモックしているクラスに送られます。ですから呼び出しがモックされているか、いないかの住み分けは完全にあなたの期待値条件によります。先立ってメソッドを定義しておく必要はありません。makePartial()メソッドは最初にパーシャルモックタイプとして紹介したオリジナルのshouldDeferMissing()と同じです。
プロキシパーシャルモック
プロキシパーシャルモックは最後の助けの綱です。finalとして記述されているため簡単にはモックすることができないクラスに出会うことがあるでしょう。同様に、finalとして記述されたメソッドを使ったクラスを見つけることもあります。このようなシナリオでは、モックにするため単純にクラスを拡張し、メソッドをオーバーライドできません。 - 創造性が必要になります。
$mock = \Mockery::mock(new MyClass);
そうです。新しいモックはプロキシです。期待値条件を条件としなくても、呼び出しを受け取り、(生成されて引数で渡された)代理を務めているオブジェクトのメソッドへ送り出します。プロキシはこの制限に影響されないため、間接的にfinalと記述されたメソッドのモックを行うことがきます。トレードオフは明らかです。クラスを拡張することができませんから、プロキシパーシャルはクラスのタイプヒントチェックには失敗します。
特別な内部動作
プロキシパーシャル以外、全てのモックオブジェクトでは、passthru()期待値条件呼び出しを使うことでどんな期待値条件がついている呼び出しも、裏に隠れている本当のクラスメソッドを実行することができます。これは本当の呼び出しの戻り値をリターンし、モックのリターンキューをバイパスします。(明らかに無視できます。)
内部で使用するための4つ目のパーシャルモックが用意されているのです。これはfinalが付けられたメソッドを含んでいるクラスをモックする場合に自動的に生成されます。そのようなメソッドはオーバーライド出来ませんから、シンプルにモックにせず残されます。通常、これを気にかける必要はありません。もし本当に、本当に、finalメソッドをモックする必要があるのであれば、唯一の可能性はプロキシパーシャルモックなのです。例えばSplFileInfoは共通クラスですが、この形式の自動内部パーシャルで扱われます。内部にpublic finalメソッドを含んでいるからです。
モックオブジェクトの検出
与えられたオブジェクトが本当のオブジェクトなのか、シミュレートしているモックオブジェクトなのかをチェックできると便利です。全てのMockeryモックは\Mockery\MockInterfaceインターフェイスを実装していますので、タイプを判断するために使用できます。
assert($mightBeMocked instanceof \Mockery\MockInterface);
デフォルトモック期待値条件
しばしばユニットテストで同じオブジェクトの依存を何度も一連のテストで使用する羽目になることがあります。それぞれのユニットテスト全てで(大量の同じコードをrequireして)このクラスやオブジェクトをモックするよりも、代わりにテストケースのsetup()メソッドで再利用できるデフォルトモックを定義することができます。これは同じもしくは似たようなモックオブジェクトに対し、ユニットテストが確認の期待値条件を使用している時でも動作します。
どの様に動作するかはデフォルト期待値条件を使いモックに定義できます。それから後ほどユニットテストで、そのテストに合わせ期待値条件を追加したり、調整したりできます。byDefault()宣言を使用することでどんな期待値条件もデフォルトとして設定できます。
パブリックなプロパティーのモック
Mockeryはいくつかの方法でプロパティをモックにできます。一番単純な方法はモックオブジェクトにシンプルにプロパティーと値をセットすることです。2つ目の方法は、その期待値が宣言されていなければ、set()とandSet()期待値条件メソッドでプロパティーの値を設定できます。
注意すべき点は、一般的にMockeryはどんなマジックメソッドのモックもサポートしていないことです。一般的に公開されるAPIではないと考えられるためです。(そのうえ運悪くモックする必要がある場合、特別扱いするために悩みの種になります!)ですからクラスに実際に宣言をされているのであれば、仮想プロパティでモックしてください。(__getと__setを使用します。)
public staticメソッドのモック
静的なメソッドは実際のオブジェクト上では呼び出されません。ですから通常のモックオブジェクトではモックできません。Mockeryはクラスエイリアスモックをサポートしており、システムのテスト環境において通常は(オートロードかrequire文で)ロードされるだろうクラスをその名前でモックします。これらのエイリアスは(required文が使われない限り - そのためオートロードを使用してください!)ローディングを阻止します。これによりMockeryは静的メソッドの呼び出しを横取りし、期待値条件を追加できるのです。
インスタンス化時にモックオブジェクトを生成する(インスタンス化モック)
インスタンス化モックとはすなわち以下の実行文です。
$obj = new \MyNamespace\Foo;
…これでも実際にモックオブジェクトを生成しています。これは実際のクラスをpublicメソッドのモックとしてインスタンスモック(エイリアスモックと似ています)で置き換えることで行います。このエイリアスはオリジナルのモックのタイプから期待値条件を取り込みます。(期待値条件を取り込んだ後、オリジナルはもう参照されず、無視されることに注意してください。)これによりオブジェクトの置き換えをシンプルに挿入出来なくても、インスタンス化を横取りできるのです。
前に述べた通り、これは実際のクラスを読み込むrequire文の実行を阻止できないため、fatal PHPエラーを発生させます。これは主要なクラスのローディング機構として、オートロードを推奨する理由です。
メソッドの参照渡し引数の動作を妨げる
PHPクラスは参照による引数を受け取ります。この場合、引数に加えられた変更は、オリジナルの変数に反映されます。(オリジナルの変数への参照がメソッドに渡されます。)簡単な例です。
class Foo { public function bar(&$a) { $a++; } } $baz = 1; $foo = new Foo; $foo->bar($baz); echo $baz; // will echo the integer 2
上記の例では、変数$bazはFoo::bar()に参照渡しされています。(パラメーターの直前の"&"に気が付きましたか?)bar()で行われた参照渡しの引数に行われた変更は、オリジナルの$baz変数に反映されます。
Mockeryは0.7以降から、(Reflectionを使用し)参照渡しされているかを確認するため、全メソッドの引数を分析し、正しく参照を処理しています。クラスによる参照への操作をどの様にモックするかですが、クロージャー引数マッチを使用します。例えば\Mockery::on()です。前記、引数の確認についてのセクションを参照してください。
例外はPHPの内部クラスです。PHPの制限によりReflectionを使用してもメソッドのパラメーターの分析ができないためです。これを解決するには、/Mockery/Configuration::setInternalClassMethodParamMap()を使用し内部クラスのメソッドパラメーターを明示的に宣言してください。
MongoCollection::insert()を使用した例を紹介しましょう。MongoCollectionはPECLのmongo拡張で提供されている内部クラスの一つです。insert()メソッドはデーターの配列を最初の引数として取り、2つ目の引数はオプションで配列を指定します。最初のデーター配列は、新しい"_id"フィールドを含んだ内容に更新されます。(つまり、insert()はリファレンス参照の引数です。)(リファレンス引数が渡される期待をMockeryに伝えるため)設定した引数マップを使い、この振る舞いをモックしましょう。クロージャーで期待されてるメソッドの引数を更新します。
このリファレンスによる参照の振る舞いを確認するPHPUnitのユニットテストです。
public function testCanOverrideExpectedParametersOfInternalPHPClassesToPreserveRefs() { \Mockery::getConfiguration()->setInternalClassMethodParamMap( 'MongoCollection', 'insert', array('&$data', '$options = array()') ); $m = \Mockery::mock('MongoCollection'); $m->shouldReceive('insert')->with( \Mockery::on(function(&$data) { if (!is_array($data)) return false; $data['_id'] = 123; return true; }), \Mockery::any() ); $data = array('a'=>1,'b'=>2); $m->insert($data); $this->assertTrue(isset($data['_id'])); $this->assertEquals(123, $data['_id']); \Mockery::resetContainer(); }
デメテルチェーンと流れるようなインターフェイス
これらの2つの言葉は、以下のような連続してつながれながら呼び出される実行文を指します。
$object->foo()->bar()->zebra()->alpha()->selfDestruct();
呼び出し側がクラスを分かっているローカルオブジェクトをそれぞれリターンしてる前提であれば、長いチェーンによるメソッドの呼び出しは必ずしも悪いわけではありません。面白い例として、Mockeryの長いチェーンでは(最初のshouldReceive()の後から)すべて同じ\Mockery\Expectationのインスタンスを呼び出します。しかしながら、時よりこれが当てはまらず、常にチェーンがオブジェクトの境界をまたぎます
どちらの場合でも、このようなチェーンのモックは大仕事になります。これを簡単に行うためにMockeryはデメテルチェーンモックをサポートしています。要約すれば、チェーンを省略型にし、最後の呼び出しの戻り値を定義することです。例えば、seofDestruct()が文字列の"Ten!"を$object(CaptainsConsoleのインスタンス)に返すとしましょう。この場合のモック方法です。
$mock = \Mockery::mock('CaptainsConsole'); $mock->shouldReceive('foo->bar->zebra->alpha->selfDestruct')->andReturn('Ten!');
上の期待値条件は前記のチェーン形式を想像できます。違いはメソッド名がシンプルな文字列で指定され、期待されている全てのチェーンは"->"でつながれている点です。実際の実装でどの様な中間オブジェクトが使用されているかには関わらず、Mockeryは自動的に期待されている呼び出しのチェーンと最終的な戻り値をセットします。
この過程では(最後の呼び出しを除き)チェーン中で使用される引数は全部無視します。
Mockery例外
Mockeryはモックオブジェクトが検証不可能な場合、3種類の例外を投げます。
- \Mockery\Exception\InvalidCountException
- \Mockery\Exception\InvalidOrderException
- \Mockery\Exception\NoMatchingExpectationException
これらの例外をtry…catchブロックで捕捉することができますし、例外メッセージとして渡される特定の情報を調べることもできます。ですがログや情報の出力にはこれとは別に提供されている例外クラスのゲッターを利用するほうが便利です。
\Mockery\Exception\InvalidCountException
この例外クラスはメソッドの実行回数が多すぎる(もしくは少なすぎる)場合に発生し、以下のメソッドを提供します。
- getMock() - モックオブジェクトの実体を返します
- getMockName() - モックオブジェクトの名前を返します
- getMethodName() - 期待値はずれだったメソッドの名前を返します
- getExpectedCount() - 期待されていた呼び出し回数を返します
- getExpectedCountComparative() - 例えば"<="のような文字列で、実際の実行回数に対する比較演算子を返します
- getActualCount() - 指定された引数の束縛に対する実際の実行回数を返します
\Mockery\Exception\InvalidOrderException
この例外クラスはordered()とglobally()期待値条件を使用時に、期待されていた順番通りにメソッドが呼び出されなかった場合に発生します。以下のメソッドを提供します。
- getMock() - モックオブジェクトの実体を返します
- getMockName() - モックオブジェクトの名前を返します
- getMethodName() - 期待値はずれだったメソッドの名前を返します
- getExpectedOrder() - この呼び出しが期待されていた順番を表す整数値を返します。
- getActualOrder() - このメソッドが呼び出された実際の順番を返します
\Mockery\Exception\NoMatchingExpectationException
この例外クラスは指定されたどの期待値条件ともメソッドの呼び出しが一致しなかった時に発生します。モックオブジェクトに指定される全ての期待値条件は名前と期待する引数のリストによりユニークに識別されます。前に説明したshouldIgnoreMissing()動作変更を指定し、この例外を無効にし、期待値外れなメソッド呼び出しに対しNULLをリターンするオプションを選択することもできます。これ例外クラスは以下のメソッドを提供します。
- getMock() - モックオブジェクトの実体を返します
- getMockName() - モックオブジェクトの名前を返します
- getMethodName() - 期待値はずれだったメソッドの名前を返します
- getActualArguments() - 一致する期待値条件を見つけるために使用した、実際の引数を返します
モックオブジェクトの記録
ある状況では、既に確立されている振る舞いパターンのテストを行なっていると気づくことでしょう。例えば、リファクタリングを行なっている時です。こうした振る舞いに対するモックオブジェクトの期待値条件を手作りする代わりに、存在するソースコードを使い、モック上で実際のオブジェクトのやり取りを期待値として記録することができます。その後、代替のバージョンやリファクタリングしたソースコードに対して記録済みの期待値を使い確認できます。
期待値を記録するために、モックするクラスの実際のインスタンスが必要です。それから実行してオブジェクトのやり取りを記録するために必要なコードを指定し、パーシャルモックを生成します。簡単なアウトラインのサンプルをご覧ください。(モックにやり取りを渡すため、クロージャーを使用しています。
とても簡単なサンプルを用意しました。あるクラス(SubjectUser)は値を取得するために別のクラス(Subject)を使用しています。(後ほどSubjectに置き換える)モックに、SubujectUserとやり取りする時の、Subjectインスタンスへの全メソッドの呼び出しと戻り値を期待値として記録しようとしています。
class Subject { public function execute() { return 'executed!'; } } class SubjectUser { public function use(Subject $subject) { return $subject->execute(); } }
記録を使うテストケースは以下の通りです。
class SubjectUserTest extends PHPUnit_Framework_TestCase { public function tearDown() { \Mockery::close(); } public function testSomething() { $mock = \Mockery::mock(new Subject); $mock->shouldExpect(function ($subject) { $user = new SubjectUser; $user->use($subject); }); /** * NewSubjectUserクラスをSubjectUserに置き換えると仮定する * SubjectUserと同じ振る舞いかどうかを確認する * つまり全く同じ方法でSbujectを使用しているかチェック */ $newSubject = new NewSubjectUser; $newSubject->use($mock); } }
NewSubjectUserがSubjectUserに関して全く同じ方法で振舞っていたら、tearDown()の中の\Mockery::colse()を呼び出したあと、モックオブジェクトを確認すれば、例外は起きていないでしょう。デフォルトでは呼び出しの順番は考慮されません。緩いマッチングが適用されます。すなわち等しければ(==)よく、同一(===)である必要はありません。
もっと厳格に行いたい場合、例えば呼び出しの順序も確認し、最後の呼び出し回数も一致しているとか、引数が全く同一であるとかも確実にチェックしたい場合は、クロージャーブロックで厳密モードで記録してください。
$mock->shouldExpect(function ($subject) { $subject->shouldBeStrict(); $user = new SubjectUser; $user->use($subject); });
Finalクラス/メソッドの取り扱い
PHPでのモックオブジェクトの一番の制限はfinal付きのクラスやメソッドをモックすることが面倒なことです。finalキーワードが付けられているとは、サブクラスで置き換えられないという事です。(サブクラス化とはクラスやモックしようとしているオブジェクトのタイプをモックオブジェクトへ継承できるということです。)
一番シンプルな解決法はclassやメソッドにfinalを付けないことです!
しかしながら機能をモックすることとタイプの安全性の折衷案として、Mockeryではfinalを付けられたクラスやfinalをつけたメソッドを含んだクラスに対し「プロキシモック」を生成できるようになっています。この機能は全ての通常のオブジェクトに友好ですが、生成結果のモックはモックしようとしているオブジェクトのクラスタイプを継承しません。すなわち、instanceof比較ではパスしません。
インスタンス化済みのモックしたいオブジェクトを\Mockery::mock()へ渡すことにより、プロキシモックを生成することができます。すなわち、Mockeryは本当のオブジェクトとのプロキシを生成し、期待値のセットや比較のためメソッドの呼び出しを選択的に横取りします。
Mockeryグローバル設定
Mockeryはいくらか調整可能なように、コアの振る舞いの小さなサブセットを保存するために設定オブジェクトをシングルトンとして利用しています。現在3つ提供しています。
- 実際に存在しないモックしようとしているメソッドの許可/不許可オプション
- 条件を満たすことがない期待値条件の存在の許可/不許可オプション(すなわち未使用期待値条件)
- 内部PHPクラスメソッドのために追加するパラメーターマップのセッター/ゲッター(Reflectionでは自動的に判別できないため)
最初の二つはデフォルトで許可されています。もちろんこれが意図しない結果を招く状況もあります。存在しないメソッドのモックは、実際の実装と動機せずに、実クラス/オブジェクトをベースとしたモックを行うためのものです。特に統合テスト(オブジェクトの結合をテストする)をある程度行わない場合に当てはまります。条件を満たさない期待値条件を許すことは不必要なモックの期待値条件が通知されないことを意味します。テストコードが乱雑になっているなら、それを読む人を混乱させる可能性があります。
以下の2行の実行、もしくは片方を実行すれば、(テストスーツ全体であろうと、選択したテスト一つだろうと)上記の振る舞いの許可/不許可を切り替えられます。
\Mockery::getConfiguration()->allowMockingNonExistentMethods(bool); \Mockery::getConfiguration()->allowMockingMethodsUnnecessarily(bool);
trueを渡せば許可され、falseなら不許可になります。両方共切り替えたその時点から有効になります。どちらも、許可されていない振る舞いが検出された時点で例外が投げられます。少くてもMockeryの柔軟性をいくらか奪ってしまうため、これらの振る舞いを不許可にする場合は十分に考慮してください。
残りの2メソッドを見てみましょう。
\Mockery::getConfiguration()->setInternalClassMethodParamMap($class, $method, array $paramMap) \Mockery::getConfiguration()->getInternalClassMethodParamMap($class, $method)
名前が内容を表していますが、内部PHPクラスのメソッドの引数を定義しています。(例えば、SPLや例えばmongo拡張のMongoCollectionのようなPECL拡張クラスです。)ReflectionではReflectionは内部クラスの引数を解析できません。多くの場合、これは必要ないでしょう。主に必要となる場面は内部クラスのメソッドがリファレンス渡しの引数を使用している場合です。 - Mockeryは内部クラスに対し自動的に追加できないため、そのようなケースでは必ず"$"シンボルを引数の指定に正しく含めてください。
予約メソッド名
多分お気づきでしょうが、例えばshouldRecieve()のように、全モックオブジェクトに直接指定して呼び出すメソッドをMockeryはいくつか使用しています。このようなメソッドは指定されたモックに、期待値条件を設定するために必要であるため、モックしようとしているクラスやオブジェクトでメソッド名の衝突を起こさぬように、実装しないでください。Mockeryが予約しているメソッドは以下の通りです。
- shouldReceive()
- shouldBeStrict()
さらに、全てのモックはそのクラス/オブジェクトには存在できない追加メソッドとprotectedのプロパティーを使用しています。少くても衝突が起きるでしょう。全てのプロパティには"mockery"のプレフィックスが付き、メソッド名には"mockery"が付きます。
PHPマジックメソッド
PHPマジックメソッドは例えば__set()のように二重の下線をプリフィックスに持ち、一般的に特定の問題をモックとユニットテストにもたらします。ユニットテストとモックオブジェクトは直接マジックメソッドを参照しないようすることを強くおすすめします。代わりに仮想メソッドとプロパティのみを参照し、マジックメソッドをシミュレートしてください。
このちょっとしたアドバイスに従えば、確実に本当のクラスのAPIをテストできますし、さらにMockeryがそれらのマジックメソッドをオーバーライドするべき場合でもコンフリクトは絶対に起きません。メソッド呼び出しとプロパティーを横取りする役割を果たすためには必然的にオーバーライドすることになります。
なるほど!(了解事項)
PHPでモックするオブジェクトには制限と了解事項があります。いくつかか機能的にモックされないか、今のところモックできません。もし皆さんがそのような状況を見つけたら、ドキュメントに記載し、可能であれば解決できるように、どうかGithubに新しいissueを作成してください。以下に留意点をリストします。
public __wakeupメソッドを含むクラスはモック可能ですが、モックした__wakeupメソッドは何も動作せず、期待値条件を設定することもできません。Mockeryは愚かにも__construct()を無効にするためにシリアライズとアンシリアライズする必要があり、さらに__wakeupメソッドをモックしようとするため、通常BadMethodCallExceptionが発生する事態を起こしますので、これは重要です。
本物ではないメソッドを使用しているクラス、つまり__callメソッドを呼び出すことになるメソッドは、最低でも一つの期待値条件を最初に定義していないと、存在しない本物ではないメソッドを呼び出したことによる例外を発生させます。(シンプルにshouldRecive()を呼び出すだけで十分です。)Mockeryにメソッド名を認識させる他の手段がないため、これは必要です。
Mockeryは実際のクラスを置き換える2つのシナリオを用意しています。インスタンスモックとエイリアスモックです。 両方共に、本当のクラスがロード済みの場合はPHP fatalエラーを発生させます。通常はrequireかinclude文によりロードされています。これらの2タイプのモックを使用する場合は適切にオートローディングを使用してください。requir()、require_once()などでファイルを指定してクラスを明示的にロードしないでください。
内部PHPクラスはReflectionを使い完全に分析することが全くできません。例えば、Reflectionはこうした内部クラスのメソッドで期待されている引数の詳細を明らかにできません。その結果、メソッド引数が参照による値を受け付けるように定義されている場合、問題を引き起こします。(Mockeryはこの状態を検出できず、スカラーと配列による値による参照と仮定します。)もし内部クラスメソッドの引数として参照することが必要な場合は、\Mockery\Configuration::setInternalClassMethodParamMap()メソッドを使用してください。
上記の了解事項は主にPHPのアーキテクチャによるものであり、不可避と考えられています。しかし、もしあなたが解決策を考えだした(もしくは現在の手法より良い方法を発見した)ならば、私達に連絡してください!
クイックサンプル
一組のメソッドコールから、結果値を返すモックを生成する
class SimpleTest extends PHPUnit_Framework_TestCase { public function tearDown() { \Mockery::close(); } public function testSimpleMock() { $mock = \Mockery::mock(array('pi' => 3.1416, 'e' => 2.71)); $this->assertEquals(3.1416, $mock->pi()); $this->assertEquals(2.71, $mock->e()); } }
メソッドコールに対し、期待値条件をチェーンで定義し、未定義のオブジェクトを返すモックを生成する
use \Mockery as m; class UndefinedTest extends PHPUnit_Framework_TestCase { public function tearDown() { m::close(); } public function testUndefinedValues() { $mock = m::mock('my mock'); $mock->shouldReceive('divideBy')->with(0)->andReturnUndefined(); $this->assertTrue($mock->divideBy(0) instanceof \Mockery\Undefined); } }
複数回queryを呼び出し、一回updateを呼び出すモックを生成する
use \Mockery as m; class DbTest extends PHPUnit_Framework_TestCase { public function tearDown() { m::close(); } public function testDbAdapter() { $mock = m::mock('db'); $mock->shouldReceive('query')->andReturn(1, 2, 3); $mock->shouldReceive('update')->with(5)->andReturn(NULL)->once(); // test code here using the mock } }
全query呼び出しがupdateの前に実行されることを期待する
use \Mockery as m; class DbTest extends PHPUnit_Framework_TestCase { public function tearDown() { m::close(); } public function testQueryAndUpdateOrder() { $mock = m::mock('db'); $mock->shouldReceive('query')->andReturn(1, 2, 3)->ordered(); $mock->shouldReceive('update')->andReturn(NULL)->once()->ordered(); // test code here using the mock } }
全queryの前にstartupを実行し、後にfinishを実行し、queryは複数の異なったパラメータで実行されることを期待するモックオブジェクトを作成する
use \Mockery as m; class DbTest extends PHPUnit_Framework_TestCase { public function tearDown() { m::close(); } public function testOrderedQueries() { $db = m::mock('db'); $db->shouldReceive('startup')->once()->ordered(); $db->shouldReceive('query')->with('CPWR')->andReturn(12.3)->once()->ordered('queries'); $db->shouldReceive('query')->with('MSFT')->andReturn(10.0)->once()->ordered('queries'); $db->shouldReceive('query')->with("/^....$/")->andReturn(3.3)->atLeast()->once()->ordered('queries'); $db->shouldReceive('finish')->once()->ordered(); // test code here using the mock } }
Copyright (c) 2010-2013, Pádraic Brady All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * The name of Pádraic Brady may not be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
追記
Laravel4では「読み書きしやすくするためコアのメソッドは静的メソッドで提供されている」状況と「静的メソッドを使用するとモックが作成しづらくなる結果、テスタビリティーが下がってしまう」という 問題を解決するため、Mockeryが統合されました。
コアクラスは全部Facadeクラスを拡張しています。その名の通りFacadeプログラムパターンを採用しています。仕組みとしてはSymfony2から影響を受けたそうです。
FacadeクラスにshouldReceiveメソッドをつけるとMockeryのモックオブジェクトが取得されます。これで、テスト時のコアクラスを簡単にモックにすることができ、ここで説明している様々な振る舞いを行わせることも、期待値条件を指定しバリデーションすることもできます。
依存性を注入するため、素のPHPで組むならば、必要なクラスを全部コンストラクタ(もしくは他の手段)による依存の注入で処理することでしょう。ではフレームワークを使用する場合はどうしましょうか。使用するコアクラスに依存を注入するためひと手間かけるべきでしょうか。
もちろん、一番の正論はコアクラスもコンストラクタによる注入を行うべきでしょう。
しかしそれですとフレームワーク、特に静的メソッドにより読みやすさを追求しているLaravelのようなフレームワークでは、記述のしやすさを犠牲にしなくてはなりません。テストの行いやすさのため、読み書きしやすさを捨てるというのは難しい決断です。読み書きのしやすさはメンテナンス性をあげるものだからです。それなら「Laravelをなぜ使うのか」という話になります。
この問題をも解決するための導入でもあると思われます。コアクラスは直接記述し、必要に応じてMockeryを利用してモックする。自分で作成したクラスは御行儀よくコンストラクタによる依存性の注入を採用するというのが、たぶんLaravelのスタンダードコーディングスタイルになるでしょう。
ただし、Mockeryのパッケージは自分でインストールする必要があります。本文で説明されています通り、ComposerのPackagistにも登録されていますので、composer.jsonに記述しアップデートをかけることでインストールされます。
もしインストールしない場合は、単純にクラスやメソッドが見つからないというエラーになります。その時はMockeryは必須パッケージではなく、テスト目的のため自分でインストールする必要があることを思い出してください。