Laravel:mockにおける引数と戻り値の関係

Date:

Share post:

遅まきながら新年明けましておめでとうございます。
昨年に引き続き、今年も毎週日曜の投稿を心がけていきたいと思います(と言いつつ1月2日はいきなりサボりましたが)。

昨年の最初の投稿もテスト関連でしたが、今年も「テストファースト」と言うことでテストの話題から。
Laravelのmock機能に関してですが、便利な機能ではあるのですが意外にまとまった解説が少なく、少し悩んだネタなので記事にしたいと思います。

まずは基本形

以下のような処理を検証したいと思います。

<?php

namespace App\Services\Sample;

class Test1Service
{
    public function __invoke(array $serviceInput)
    {
        $Test1Repository = app()->make(\App\Repositories\Test1Repository::class);

        $resultObj = new \stdClass;
        $resultObj->result1 = $Test1Repository(1);
        return $resultObj;
    }
}

上記で戻した「stdClass」インスタンスの内容に従ってAPIの戻りデータ(JSON)を整形しますが、その辺は重要ではないので割愛します。

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

    /**
     * @test
     */
    public function Sample_Test1Controller_基本() : void
    {
        $this->mock(\App\Repositories\Test1Repository::class, function($mock) {
            $mock->shouldReceive('__invoke')->with(1)->andReturn(10);
        });        

        // 実行
        $response = $this->get(route('sample.test1'));

        // 確認
        $response->assertStatus(200);
        $expected = [
            'data' => [
                'result1' => 10,
            ],
        ];
        $response->assertExactJson($expected);
    }

クラス「\App\Repositories\Test1Repository」の挙動をmockで代替しようとしている訳です。
同クラスは1を入力すると10を返す機能を有していることが期待されており、それをテストしています。

ポイントはメソッド「with」です。
mockの例としては置換対象メソッドを指定する「shouldReceive」や戻り値を指定する「andReturn」に関して書かれたものはよく見かけるのですが(そもそもこの2つが特定できないとテスト自体ができないのですが)、引数の確認に関して言及している記事は割と少ない印象です。
と言うことで、「with」では引数として入力されることが想定される値を指定します。このように指定しておくと引数が不一致の場合はエラーになります。

形的にも、どのメソッドに対して、何を入力し、何が出力されるか、と言うことが揃っているので、バランスが良い印象です。
積極的に「with」は指定していきたいと思います。

同じクラス・メソッドの複数呼び出し

上記「Test1Service」を以下のように変更します。

<?php

namespace App\Services\Sample;

class Test1Service
{
    public function __invoke(array $serviceInput)
    {
        $Test1Repository = app()->make(\App\Repositories\Test1Repository::class);

        $resultObj = new \stdClass;
        $resultObj->result1 = $Test1Repository(1);
        $resultObj->result2 = $Test1Repository(1);
        return $resultObj;
    }
}

上記に対して以下のテストケースは成立します。

    /**
     * @test
     */
    public function Sample_Test1Controller_基本() : void
    {
        $this->mock(\App\Repositories\Test1Repository::class, function($mock) {
            $mock->shouldReceive('__invoke')->with(1)->andReturn(10);
        });        

        // 実行
        $response = $this->get(route('sample.test1'));

        // 確認
        $response->assertStatus(200);
        $expected = [
            'data' => [
                'result1' => 10,
                'result2' => 10,
            ],
        ];
        $response->assertExactJson($expected);
    }

要は、mockの設定において特定のメソッドに対して特定の引数が与えられた場合に特定の値を返すと言う仕組みは、基本的には回数指定なしで機能すると言うことです。
逆に、一度しか実行されないことが想定される場合は「once」メソッドの呼び出しを付加することで、複数回呼び出された場合はエラーになるようにできます。

$mock->shouldReceive('__invoke')->with(1)->andReturn(10)->once();

では、同じメソッドに対して引数を変えながら複数回呼び出しを行う場合はどうすれば良いでしょう?
実は簡単で、同じメソッドを指定しつつ、異なる引数と戻り値の組み合わせを羅列すれば良いだけです。

Test1Service」を以下のように変更します。

<?php

namespace App\Services\Sample;

class Test1Service
{
    public function __invoke(array $serviceInput)
    {
        $Test1Repository = app()->make(\App\Repositories\Test1Repository::class);

        $resultObj = new \stdClass;
        $resultObj->result1 = $Test1Repository(1);
        $resultObj->result2 = $Test1Repository(2);
        return $resultObj;
    }
}

これに対して、テストケースは以下のようにします。

    /**
     * @test
     */
    public function Sample_Test1Controller_基本() : void
    {
        $this->mock(\App\Repositories\Test1Repository::class, function($mock) {
            $mock->shouldReceive('__invoke')->with(1)->andReturn(10);
            $mock->shouldReceive('__invoke')->with(2)->andReturn(200);
        });        

        // 実行
        $response = $this->get(route('sample.test1'));

        // 確認
        $response->assertStatus(200);
        $expected = [
            'data' => [
                'result1' => 10,
                'result2' => 200,
            ],
        ];
        $response->assertExactJson($expected);
    }

引数が1の場合は10を返し、2の場合は200を返す処理を想定したテストが実施できます。

なお、引数と戻り値の組み合わせの指定の順序は実際の処理における呼び出し順序とは無関係です。
上記例で言えば、以下のように指定の順序を逆転させても問題なく動作します。

$mock->shouldReceive('__invoke')->with(2)->andReturn(200);
$mock->shouldReceive('__invoke')->with(1)->andReturn(10);

同じ引数での複数呼び出しに対して戻り値を変えたい場合

そのようなケースが存在するかどうか分かりませんが、同一クラス・メソッドに対して同じ引数で複数回呼び出しつつ、その都度戻り値を変えたいケースも試してみました。

Test1Service」を以下のように変更します。

<?php

namespace App\Services\Sample;

class Test1Service
{
    public function __invoke(array $serviceInput)
    {
        $Test1Repository = app()->make(\App\Repositories\Test1Repository::class);

        $resultObj = new \stdClass;
        $resultObj->result1 = $Test1Repository(1);
        $resultObj->result2 = $Test1Repository(1);
        return $resultObj;
    }
}

2回とも引数は1です。

これに対してテストケースは以下のようにします。

    /**
     * @test
     */
    public function Sample_Test1Controller_基本() : void
    {
        $this->mock(\App\Repositories\Test1Repository::class, function($mock) {
            $mock->shouldReceive('__invoke')->with(1)->andReturn(10)->once();
            $mock->shouldReceive('__invoke')->with(1)->andReturn(200)->once();
        });        

        // 実行
        $response = $this->get(route('sample.test1'));

        // 確認
        $response->assertStatus(200);
        $expected = [
            'data' => [
                'result1' => 10,
                'result2' => 200,
            ],
        ];
        $response->assertExactJson($expected);
    }

ポイントは「once」メソッドを利用する点です。これを行わないと、複数回呼び出しに対して常に最初の設定が有効になってしまうため、200を戻す処理は実行されません。言い換えれば、上記のように「once」メソッドで実行回数を指定することで2回目の呼び出しに関しては200を戻す方の処理が有効になります。

当然ながら、このケースでは引数と戻り値の組み合わせ順序が重要になります。以下のように順序を逆転させてしまうと期待した結果が得られません。

$mock->shouldReceive('__invoke')->with(1)->andReturn(200)->once();
$mock->shouldReceive('__invoke')->with(1)->andReturn(10)->once();

さらには「once」メソッド指定の特性により、同じ引数・戻り値の組み合わせを複数使用することもできるようです。

Test1Service」を以下のように変更します。

<?php

namespace App\Services\Sample;

class Test1Service
{
    public function __invoke(array $serviceInput)
    {
        $Test1Repository = app()->make(\App\Repositories\Test1Repository::class);

        $resultObj = new \stdClass;
        $resultObj->result1 = $Test1Repository(1);
        $resultObj->result2 = $Test1Repository(1);
        $resultObj->result3 = $Test1Repository(1);
        return $resultObj;
    }
}

Test1Repository」の呼び出しを1つ増やしました。

テストケースは以下の通り。

    /**
     * @test
     */
    public function Sample_Test1Controller_基本() : void
    {
        $this->mock(\App\Repositories\Test1Repository::class, function($mock) {
            $mock->shouldReceive('__invoke')->with(1)->andReturn(10)->once();
            $mock->shouldReceive('__invoke')->with(1)->andReturn(200)->once();
            $mock->shouldReceive('__invoke')->with(1)->andReturn(10)->once();
        });        

        // 実行
        $response = $this->get(route('sample.test1'));

        // 確認
        $response->assertStatus(200);
        $expected = [
            'data' => [
                'result1' => 10,
                'result2' => 200,
                'result3' => 10,
            ],
        ];
        $response->assertExactJson($expected);
    }

引数1を渡して戻り値10を得る処理が、引数1を渡して戻り値200を得る処理を挟んで2回実行されることを想定しています。
このテストケースは正常に動作します。

つまりは「once」指定したメソッドはその引数と戻り値の組み合わせが設定順に記録され、実行時にはその順序で照合されると言うことのようです。

この方式が採用できるのであれば、テストの柔軟性がかなり向上しますね。

総括

結果だけ見ると別段特殊性はないように思いますが、同じクラス・メソッドに対して引数と戻り値の組み合わせを複数指定できると言う発想に至るまでに少々手間取りましたので、備忘録的に情報を残しておく意味も含めて紹介させていただきました。

特に同じ引数・戻り値の組み合わせを複数回指定できるのは個人的にはかなり意外で、改めてLaravelって色々とできるフレームワークだと実感しました(この場合はMockeryが優秀と言うことかもしれませんが)。

mock機能を使いこなせることとテストの効率は非常に関係が深い(と思っている)ので、今後も精進していきたいと思います。

Related articles

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

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

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

以前から作成したいと考えていたのですが...

Laravel Filamentを使用した管理画面...

前回Breezeをインストールしたこと...

Laravel Filamentを使用した管理画面...

前回、filamentでのリソース作成...