Laravel:モック

Date:

Share post:

以前「LaravelのUI層」と言う記事で少し触れたのですが、テストにおいて以下のようにしてモックが使えます。

例えば、コントローラとして以下のようなものが存在するとします。

<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;

// Service
use App\Services\DummyService;

class DummyController extends Controller
{
    public function __invoke(DummyService $service)
    {
        return $service();
    }
}

ここでクラス「DummyService」の実体が存在しなかったとしても、以下のようにモックで代替できます。

<?php

namespace Tests\Feature;

use Tests\TestCase;

class DummyTest extends TestCase
{
    public function test() : void
    {
        // モック
        $this->mock(\App\Services\DummyService::class, function ($mock) {
            $mock->shouldReceive('__invoke')
                 ->andReturn(json_encode(['result' => 'XYZ']));
        });

        // 実行
        $response = $this->json('get', route('dummy'));

        // 確認
        $response->assertStatus(200);
        $expected = [
            'result' => 'XYZ'
        ];
        $response->assertExactJson($expected);
    }
}

上記テストケースはモックによってクラス「DummyService」の代わりに「json_encode([‘result’ => ‘XYZ’])」を返す動作を確認できます。

なお、モックは必ずしも実在しないクラスを代替するものではありません。実在するクラスに関しても対象クラス名・メソッド名を適切に指定すれば、そのクラス・メソッドを代替できます。
むしろ、こちらの目的で使用される場合が多いのではないでしょうか?

例えば、外部のAPIを使用するようなケースを考えます。
このような場合、相手側がテスト的な使用を許容していれば良いですが、そうでない場合はテスト時には同APIの呼び出しを実行しないようにする必要があります。しかも、同APIの呼び出し前後の処理のテストは行いたいので、あたかもAPI呼び出しが行われ、結果が返ってきたような状況を作りたくなります。
このような時にモックが有効な訳です。

サービスコンテナ

これは便利ということで、以下のようなことを考えました。

先のコントローラはそのまま、クラス「DummyService」として以下のようなものを用意します。

<?php

namespace App\Services;

use App\Repositories\DummyRepository;

class DummyService
{
    public function __invoke()
    {
        $DummyRepository = new DummyRepository;
        $result = $DummyRepository();
        return json_encode([
            'result' => $result
        ]);
    }
}

上記に対して以下のようなテストケースを考えます。

<?php

namespace Tests\Feature;

use Tests\TestCase;

class DummyTest extends TestCase
{
    public function test() : void
    {
        // モック
        $this->mock(\App\Repositories\DummyRepository::class, function ($mock) {
            $mock->shouldReceive('__invoke')
                 ->andReturn('XYZ');
        });

        // 実行
        $response = $this->json('get', route('dummy'));

        // 確認
        $response->assertStatus(200);
        $expected = [
            'result' => 'XYZ'
        ];
        $response->assertExactJson($expected);
    }
}

今度はクラス「DummyService」内で使用しているクラス「DummyRepository」を代替させようと言う訳です。

で、実行してみると…期待通りにクラス「DummyRepository」をモックで代替できません。

実は本記事の最初に示した例では、クラス「DummyService」のインスタンス生成を自力では行っていません。
コントローラの引数としてタイプヒンティングしておくだけで自動的に当該クラスのインスタンスが渡されてきています。
これはLaravelのサービスコンテナという機能によるもので、上記モックの使い方はサービスコンテナによってインスタンス生成する場合のみ有効のようです。

と言うことで、クラス「DummyRepository」をサービスコンテナ経由でインスタンス化するように処理を変更してみます。

<?php

namespace App\Services;

use App\Repositories\DummyRepository;

class DummyService
{
    public function __invoke()
    {
        $DummyRepository = app()->make(DummyRepository::class);
        $result = $DummyRepository();
        return json_encode([
            'result' => $result
        ]);
    }
}

今度は期待通りにモックによる代替処理が行われます。

overload

実は先ほど失敗していたnewによるインスタンス化に関してもモックする方法はあります。「overload」と呼ばれる方法です。
ただ、個人的に現在確認できている限りでは、上記例にあったようにマジックメソッド「__invoke」を使用するような書式では正常に動作させることはできませんでした。

期待したような動作になったケースは以下の通り。

まずはクラス「DummyService」です。

<?php

namespace App\Services\Util;

use App\Repositories\DummyRepository;

class DummyService
{
    public function __invoke()
    {
        $DummyRepository = new DummyRepository;
        $result = $DummyRepository->proc();
        return json_encode([
            'result' => $result
        ]);
    }
}

クラス「DummyRepository」に関しては、今までのようにメソッド「__invoke」によりインスタンス自体を関数のように使用するのではなく、普通にメソッド「proc」を用意し、これを呼び出すようにしています。

上記に対してテストケースは以下のようになります。

<?php

namespace Tests\Feature;

use Tests\TestCase;

class DummyTest extends TestCase
{
    public function test() : void
    {
        // モック
        $this->mock('overload:\App\Repositories\DummyRepository', function ($mock) {
            $mock->shouldReceive('proc')
                 ->andReturn('XYZ');
        });

        // 実行
        $response = $this->json('get', route('dummy'));

        // 確認
        $response->assertStatus(200);
        $expected = [
            'result' => 'XYZ'
        ];
        $response->assertExactJson($expected);
    }
}

上記のように対象クラス名に「overload」と言うプレフィックスを付けることで、newにより生成されるインスタンスをモックすることも可能になります。

先のサービスコンテナを使う形であれば、そのサービスコンテナに対して特定のクラスのすり替えを指示することはできそうな気がします。
しかし、newによるインスタンス化のようなシンプルな(Laravelの仕組みが関与していないように見える)方法においてもすり替えが可能な点はよく考えてみると意外です。

この辺はLaravelがクラスファイルをオートロードしていることと関係するようですが、深く追求するのは止めておきます…

総括

とりあえず「overload」なる方法があることは分かりましたが、少なくとも現時点の私の知識では制約のある使い方しかできませんし、サービスコンテナを使う方法で問題ないため、そちらを使えば良いかと思っています。

個人的にはテストケースを書いていてモックを使いたいケースと言うのはあまり多くなく、たまに使いたくなった際には用法を忘れていて再度Google先生に教えを乞うことになってしまいます。
この辺でモックの使い方やサービスコンテナとの関連もしっかり覚えておくようにしたいと思います(願望)。

Related articles

ローカルSMTPメールサーバ(Mailpit)をE...

ローカル環境でのメール送受信テストにつ...

EC-CUBE 4系のプラグイン開発について その...

今回は、ちょっとハマったプラグインのイ...

EC-CUBE 4系のプラグイン開発について その...

前回のブログの最後でちょっと書いたので...

EC-CUBE 4系のプラグイン開発について その...

前回、プラグインを一旦有効化させて管理...