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のパーシャルモック、わかってしまえば、使えますよ。