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