Laravelのテストでモックを利用する

今回はテストを書く中で実際に書いたモックの例を晒しておきます。LaravelのファサードはshouldReceiveで非常に簡単にモック化できるので、ぼくみたいなテスト勉強中の人でも覚えることも少なくとても使いやすいと思いました。

メール送信をモック化する

メールを送信する例は下記のようになります。例はonceを付けているので必ず1回だけ呼ばれないとエラーになります。またwithで引数の値も厳格に見てるので違うものが渡された場合はエラーになります。引数が何でも良い場合はwithAnyArgsなどを使います。

\Mail::shouldReceive('send')->once()->with(
    ['text' => 'emails.template1'],
    Mockery::on(function ($data) {
        return true;
    }),
    Mockery::on(function (\Closure $closure) use ($params) {
        $mock = Mockery::mock('Illuminate\Mail\Message');
        $mock->shouldReceive('from')
             ->once()
             ->with(
                 config('sender@example.com'),
                 config('Sender Name')
             )
             ->andReturn($mock);
        $mock->shouldReceive('to')
             ->once()
             ->with($params['email'])
             ->andReturn($mock);
        $mock->shouldReceive('bcc')
             ->once()
             ->with(config('bcc@example.com'))
             ->andReturn($mock);
        $mock->shouldReceive('subject')
             ->once()
             ->with("{$params['name']}さん、はじめまして!")
             ->andReturn($mock);
        $closure($mock);
        return true;
    })
)->andReturn(true);

テスト時のI/Oを減らしたいので呼ば出さないようにしたい場合は単に下記のようにすればOKです。

\Mail::shouldReceive('send')->andReturn(true);

ストレージ出力をモック化する

ぼくの場合はS3にアップロードする処理が多かったんですが、これも簡単にモック化する事ができます。下記のやり方はStackOverflowか何かで見つけたやり方(URLを失念…)ですが、手順としてはまずStorageを拡張しモック化されたStorageオブジェクトが返るようにしています。ちなみに下記の例ではonceなどはつけていないので呼び出されなくても何度呼びだされてもエラーは発生しません。

/**
 * StorageをMock化する
 */
protected function mockStorage()
{
    $storage = $this->getStorageMock();

    $storage->shouldReceive('put')->andReturn(true);
    $storage->shouldReceive('url')->andReturn('http://example.com/test1.jpg');
    \Storage::shouldReceive('disk')->with('s3')->andReturn($storage);
}

/** 
 * Mock化したStorageオブジェクトを取得する
 * @return \Illuminate\Contracts\Filesystem\Filesystem
 */
protected function getStorageMock()
{
    \Storage::extend('mock', function () {
        return Mockery::mock(\Illuminate\Contracts\Filesystem\Filesystem::class);
    });
    Config::set('filesystems.disks.mock', ['driver' => 'mock']);
    Config::set('filesystems.default', 'mock');
    return Storage::disk('mock');
}

自前のコードもファサード化しておくとモック化が容易

LaravelのファサードはshouldReceiveを使って一様にモック化できるので覚える事がとても少ないです。なので自前のユーティリティメソッドなどもファサード化しておくとテストの時にとても助かります。下記は自前の緯度経度取得ファサードGeocoderをモック化するコードです。

$mock = Mockery::mock();
$mock->shouldReceive('getLatitude')->once()->andReturn(3);
$mock->shouldReceive('getLongitude')->once()->andReturn(4);
$mock->shouldReceive('getAdministrativeArea')->once()->andReturn('愛知県');
$mock->shouldReceive('getLocality')->once()->andReturn('名古屋市');
$mock->shouldReceive('getLocalityWard')->once()->andReturn('緑区');
$mock->shouldReceive('getSubLocality')->once()->andReturn('滝の水');
\Geocoder::shouldReceive('geocode')->once()->andReturn($mock);

番外編)ファサード化されてない場合に取得されるオブジェクトをモックに差し替えたい場合

ファサード化しているとモック化するのが非常に簡単なのですが、それが出来ない場合のコードの例です。Laravelあまり関係ない例です。この例ではモック化したいインスタンスがFactory::getInstanceという静的メソッドで生成されるのですが、このgetInstanceをMockeryでインターセプトしてモックオブジェクトを返すという処理を入れています。Mockeryすごいですね。

$client = Mockery::mock(\App\Client::class);
$mockFactory = Mockery::mock('alias:' . \App\HogeFactory::class);
$mockFactory->shouldReceive('getInstance')->once()->andReturn($client);

$client->shouldReceive('hello')->once()->with('Taro')->andReturn('Hello Taro');

上記のコードをテストの冒頭で実行しておく事で、HogeFactory::getInstanceで取得できるインスタンスはモックになります。

ただ、このMockery::mock(‘alias:<クラス名>’)はクラスローディングの関係で上記の例で言えばこのテスト時に初めてHogeFactoryをロードできている必要があります。そのためにテストのメソッドではアノテーションを下記のように設定して実行します。

/**
 * @runInSeparateProcess
 * @preserveGlobalState disabled
 */
public function testインスタンスをモックに差し替えるテスト()
{
    $client = Mockery::mock(\App\Client::class);
    $mockFactory = Mockery::mock('alias:' . \App\HogeFactory::class);
    $mockFactory->shouldReceive('getInstance')->once()->andReturn($client);

    $client->shouldReceive('hello')->once()->with('Taro')->andReturn('Hello Taro');

ファサード化してなくても上記のようにモック化はなんとかできるんですが、LaravelだったらLaravelらしくファサード化しておくのが一番という事ですね。これはちょっとめんどかったです。