Mockeryでファンクションをモックする

タグ: Mockery   テスト志向   Laravel4  

ユニットテストのお話です。グローバルのファンクションを使用すると、テストがしづらくなると言われています。置き換えができないからです。

追記:内容はやや古くなりました。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のパーシャルモック、わかってしまえば、使えますよ。