Masayan tech blog.

  1. ブログ記事一覧>
  2. 【Laravel】Mockeryで外部APIをモック化してテストを実行できるようにする

【Laravel】Mockeryで外部APIをモック化してテストを実行できるようにする

公開日

環境

  • windows10
  • DockerDesktop for Win 3.5.x
  • Laravel 8.x
  • PHP 8.x
  • mockery 1.4.2
  • VsCode
  • gitbash 2.32.0.1

モックの何がうれしいのか

モックを使用することにより、例えば、あるクラスAが依存している以下のようなものがまだ整備されていない状態において、クラスAのテストを記述することが可能です。

  • クラスB(Aの中でBのインスタンスを作成して使用する)
  • 外部API(Aの中で外部APIを扱う)

前提

例として、楽天商品検索APIに依存しているクラスをモックを使ってテストケースを作成してみたいと思います。

  • 楽天商品検索APIに依存しているクラス
    RakutenProductSearchService.php
  • 楽天商品検索APIに依存しているクラスのテストクラス
    RakutenProductSearchServiceTest.php
  • 楽天商品検索APIに依存しているクラスを呼び出しているコントローラークラス
    RakutenProductSearchController.php

RakutenProductSearchService.php

楽天APIからデータを検索して取得する責務を持つクラスです

<?php
namespace Zaico\Application\RakutenProduct;

use Illuminate\Http\Request;
use RakutenRws_Client;
use Zaico\Domain\RakutenItem\RakutenItem;
use Zaico\Domain\RakutenItem\RakutenItemTransformer;

class RakutenProductSearchService
{
    private RakutenRws_Client $client;
    private RakutenItem $rakutenItem;

    public function __construct(
        RakutenRws_Client $client,
        RakutenItem $rakutenItem
    ) {
        $this->client = $client;
        $this->rakutenItem = $rakutenItem;
    }

    public function exec(Request $request, mixed $rakutenAppId): array
    {
        $this->client->setApplicationId($rakutenAppId);
        $responseData = $this->search($request->input('barcode'));
        $this->toRakutenItemDomain($responseData);

        return array_map(
            fn($rakutenItem) => RakutenItemTransformer::transform($rakutenItem),
            [$this->rakutenItem]
        );
    }

    private function search(string $barcode): mixed
    {
        if (!empty($barcode)) {
            $response = $this->client->execute('IchibaItemSearch', [
                'keyword' => $barcode,
            ]);

            if (!$response->isOk()) {
                echo 'Error:' . $response->getMessage();
            }
            return $response->getData();
        }
    }
    private function toRakutenItemDomain(mixed $responseData): void
    {
        $this->rakutenItem
            ->setName($responseData['Items'][0]['Item']['itemName'])
            ->setUrl($responseData['Items'][0]['Item']['itemUrl'])
            ->setImageUrl(
                $responseData['Items'][0]['Item']['mediumImageUrls'][0][
                    'imageUrl'
                ]
            );
    }
}

RakutenProductSearchServiceTest.php

楽天APIからデータを検索して取得する責務を持つクラスを、モックで置き換えたテストケースです

<?php
namespace Tests\Feature;

use Illuminate\Http\Request;
use PHPUnit\Framework\TestCase;
use Zaico\Application\RakutenProduct\RakutenProductSearchService;

class RakutenProductSearchServiceTest extends TestCase
{
    /**
     * @test
     */
    public function 楽天商品を検索して取得できること()
    {
        $mock = \Mockery::mock(RakutenProductSearchService::class);
        $mock
            ->shouldReceive('exec')
            ->once()
            ->andReturn([
                'name' => 'ガム',
                'url' => 'test url',
                'imageUrl' => 'test image url',
            ]);

        $request = new Request([]);
        $this->assertEquals(
            [
                'name' => 'ガム',
                'url' => 'test url',
                'imageUrl' => 'test image url',
            ],
            $mock->exec($request, '483949379')
        );
    }
}

RakutenProductSearchController.php

/products/searchを叩いたら、呼び出されるコントローラーです。indexメソッドで楽天APIからデータを検索して取得する責務を持つクラスを呼び出します。

<?php
namespace App\Http\Controllers;

use Illuminate\Http\Request;
use RakutenRws_Client;
use Zaico\Application\RakutenProduct\RakutenProductSearchService;
use Zaico\Domain\RakutenItem\RakutenItem;

class RakutenProductSearchController extends Controller
{
    /**
     *
     * @param RakutenProductSearchService $rakutenProductSearchService
     * @param Request $request
     * @param RakutenRws_Client $client
     * @param RakutenItem $rakutenItem
     * @return array
     */
    public function index(
        RakutenProductSearchService $rakutenProductSearchService,
        Request $request,
        RakutenRws_Client $client,
        RakutenItem $rakutenItem
    ): array {
        return $rakutenProductSearchService->exec(
            $request,
            config('app.rakuten_id')
        );
    }
}

モックを用いたテスト

ポイントは以下です。

  • \Mockery::mock(モック化したいクラス::class)で、モックオブジェクトを生成
  • $mock->shouldReceive('モック化したいクラスの呼び出されるべきメソッド名')を指定する
  • $mock->with()で期待されるメソッドの引数を指定する
  • $mock->once()でメソッドが呼び出される回数を1回として指定する
  • $mock->andReturnでメソッドを実行したことにより期待される戻り値を指定する
  • あとは、$mock->exec()でメソッドを実行し、その返り値が期待しているものと等しいかどうかをアサ―トとする
<?php
namespace Tests\Feature;
use Illuminate\Http\Request;
use PHPUnit\Framework\TestCase;
use Zaico\Application\RakutenProduct\RakutenProductSearchService;

class RakutenProductSearchServiceTest extends TestCase
{
    /**
     * @test
     */
    public function 楽天商品を検索して取得できること()
    {
        $mock = \Mockery::mock(RakutenProductSearchService::class);
        $mock
            ->shouldReceive('exec')
       ->with($request, '483949379')
            ->once()
            ->andReturn([
                'name' => 'ガム',
                'url' => 'test url',
                'imageUrl' => 'test image url',
            ]);

        $request = new Request([]);
        $this->assertEquals(
            [
                'name' => 'ガム',
                'url' => 'test url',
                'imageUrl' => 'test image url',
            ],
            $mock->exec($request, '483949379')
        );
    }
}

withで指定した引数が期待される内容と異なっている場合は、以下のようにエラーを表示してくれる

the method was unexpected
or its arguments matched no expected argument list for this method

onceを付与すると、1回呼び出されることを保証してくれるので、一度も呼び出さなかったら以下のようなエラーを出してくれる

Method exec(<Any Arguments>) from Mockery_0_Zaico_Application_RakutenProduct_RakutenProductSearchService should be called
exactly 1 times but called 0 times.

以上です。モックを使うことのメリットを少しでもわかっていただけたら幸いです。

公式Doc

まとめ

いかがでしたでしょうか。本記事では、Laravel8でMockeryで外部APIをモック化してテストを実行できるようにするための手順や使用方法について紹介しています。モックを使用することにより、例えば、あるクラスAが依存している以下のようなものがまだ整備されていない状態において、クラスAのテストを記述することが可能になる等メリットがたくさんありますので、ぜひ参考にしてみてください。