Mockeryでファンクションをモックする
ユニットテストのお話です。グローバルのファンクションを使用すると、テストがしづらくなると言われています。置き換えができないからです。
追記:内容はやや古くなりました。Mockery1.0.0のドキュメントを翻訳しましたので、そちらを主に参考にしてください。
https://readouble.com/mockery/1.0/ja/index.html
- <?php
- class Sample
- {
- function echoFile( $filename )
- {
- echo file_get_contents( $filename );
- }
- }
これがテストしづらいのは、このままではダミーのファイルを作成し、それを読み込んで表示した内容を目視で比較しなくては、動作確認ができないからです。(まあ、出力結果をファイルにリダイレクトすることもできますが、手間なのには変わりありません。)
そこでLaravelを始めとするフレームワークでは、よく使用するこの手の関数をクラスにまとめています。この場合のfile_get_contentsは、Laravelの場合、Fileクラスに含まれています。
- function echoFile( $filename )
- {
- echo File::get( $filename );
- }
これでPHPUnitからテストが簡単に行われます。Laravelの場合、Fileクラスはファサードで、モックへの置き換えが特に簡単です。
- function testEchoFile()
- {
- File::shouldReceive('get')->andReturn('お好きな内容');
- ...
- }
呼び出しているFileクラスのメソッドが一回なら、これで自動的に置き換えられます。(複数の場合はMockeryのモックオブジェクトを取得し、それに条件を付け加えます。)
どうしてFileクラスが存在しているのか、テストが必要がない人は理解が難しい部分です。全く、関数をそのまま呼び出しているものが多いからです。理由はテストが簡単になるからです。
さて、全部の関数がLaravelへ用意されたクラスに取り込まれているわけではありません。
- <?php
- class Sample
- {
- function echoTime( )
- {
- echo time( );
- }
- }
これを取り扱うためのテストライブラリーもありますが。ほんのちょっとの工夫でMockeryで処理可能です。
好ましいのはクラスを作成し、そこにまとめることですが、これは管理が煩雑に生る欠点もあります。そこで、グローバル関数の呼び出しは、そのままメソッドに分けましょう。
- class Sample
- {
- function echoTime( )
- {
- echo $this->time( );
- }
- function time() {
- return time()
- }
- }
通常、ユニットテストする場合はSampleをそのままnewで生成しますが、このような場合はパーシャルモックにします。そのパーシャルモックを通してテストします。
- function testEchoDate()
- {
- $sample = Mockery::mock('Sample')->makePartial();
- $sample->shouldReceive('time')->andReturn(お好きな数字);
- ...
- }
->makePartial()
でパーシャルモックにすると、呼び出しが定義されて場合のみモックされます。その他の呼び出しは、もとのクラスのオブジェクトを呼び出してくれます。つまり、この形式のモックは、モックオブジェクトの中に本当のオブジェクトを持っており、モックしたい部分の条件を書いておけば、当てはまるメソッドの呼び出しだけをモックし、それ以外は本当のオブジェクトのメソッドを呼び出してくれます。
名前空間付きで、コンストラクターへの引数が必要な場合の指定は、以下のサンプルから読み取って下さい。
- // テスト対象
- namespace Root\Sub;
- class Sample
- {
- public function __construct( $arg1, $arg2 )
- {
- $this->arg1 = arg1;
- $this->arg2 = arg2;
- }
- public function doSomething()
- {
- return $this->time();
- }
- public function time()
- {
- return time();
- }
- }
- // テストクラス
- use Mockery as M;
- class SampleTest
- {
- public function testDoSometing()
- {
- $testArg1 = 'コンストラクター引数1';
- $testArg2 = 'コンストラクター引数2';
- $expected = 200000;
- $sample = M::mock( 'Root\Sub\Sample', array( $testArg1, $testArg2 ) )
- ->makePartial();
- $sample->shouldReceive( 'time' )
- ->andReturn( $expected );
- $this->assertEquals( $expected, $sample->doSomething() );
- }
- }
Mockeryのパーシャルモック、わかってしまえば、使えますよ。