「Laravelで実装したAPI」と「OpenAPIの定義ファイル」に相違が無いことをテストしたい

会社で「SwaggerHubを導入するから」と担当を任され、
既存のAPIにOpenAPIを導入する方法を検討中のtenryuです。

多くのOpenAPI導入記事で**作成した仕様書とAPIに相違が発生しないよう**テストが実施されており、
弊社のSwaggerHub導入でも必須だと感じました。

私の担当プロジェクトでは、APIの実装にPHPフレームワークのLaravelが使用されています。

「Laravelで実装したAPI」と「仕様書」に相違が無いことを確認するため,
[openapi-psr7-validator](https://github.com/thephpleague/openapi-psr7-validator)というライブラリを利用しテストしている記事がいくつかありました。

しかし、わかりにくい部分が多々あったり、そもそも情報量が少なかったのでまとめたいと思います。

# openapi-psr7-validator
https://github.com/thephpleague/openapi-psr7-validator

### How To Validateでいきなり躓く
使い方がよくわからない・・・
ServerRequest Messageのバリデーション？

いろいろ検索して出てくるのは、**PSR-7に変換してからではないと使えない**ということ
LaravelのリクエストとレスポンスはPSR-7には準拠していないということ？

### そもそもPSR-7って何？
PSR(PHP Standards Recommendations)と言われるPHPコーディング規約の一つで、**HTTPのリクエスト、レスポンスに関する規約**らしい

https://inside.pixiv.blog/2022/03/30/150000

### LaravelとPSR-7
https://readouble.com/laravel/8.x/ja/requests.html#:~:text=expectsJson())%20%7B%0A%20%20%20%20//%20...%0A%7D-,PSR%2D7%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88,-PSR%2D7%E6%A8%99%E6%BA%96

> PSR-7標準は、リクエストとレスポンスを含むHTTPメッセージのインターフェイスを規定しています。Laravelリクエストの代わりにPSR-7リクエストのインスタンスを取得したい場合は、最初にいくつかのライブラリをインストールする必要があります。LaravelはSymfony HTTP Message Bridgeコンポーネントを使用して、通常使用するLaravelのリクエストとレスポンスをPSR-7互換の実装に変換します。

Laravelの日本語マニュアルを確認すると、Laravelのリクエスト、レスポンスはPSR-7準拠ではないらしい
PSR-7互換にするにはライブラリをインストールする必要があるとのこと


# ライブラリ使用方法の参考
https://qiita.com/imunew/items/87fffc115cbf214fb750

https://fortee.jp/phpcon-2021/proposal/5a58e077-6de6-453a-abff-adbe24cc3156

https://github.com/sun-yryr/phpcon2021-openapi

一番上のQiitaの記事は古く流用できる部分が少ないが、使い方のイメージは沸いた。

特に参考になったのが、PHP Conference Japan 2021で登壇されていた**株式会社ゆめみの皆川さん**の発表とリポジトリ。
YouTubeで発表を聞き、リポジトリを確認したらかなり理解できた。

# 実際に使ってみる
ユーザー情報を取得する簡単なAPIを実装しテストしてみる

### 環境構築
Laravelの環境構築にはいつもお世話になってる**ucanさん**の記事を使わせていただいた
マルチステージビルドに対応するなど、以前よりもさらに使いやすくなっていた

https://qiita.com/ucan-lab/items/5fc1281cd8076c8ac9f4

https://github.com/ucan-lab/docker-laravel

テストコードをデバッガで確認しようと思い、Xdebugをインストールした
ただ、テストコードはXdebugを使っても止まらず・・・・
テストコードってデバッグできないの？
テストコード程度、皆さんはデバッグしないのだろうか・・・

インストール方法はリポジトリのWikiで確認できる。

https://github.com/ucan-lab/docker-laravel/wiki/Setup-Xdebug

### APIの実装
Resourceなどは使わず、ただ単にユーザーのidとnameを返すだけ。
```php:api.php
<?php

use Illuminate\Support\Facades\Route;

Route::get('/user', function() {
    return response()->json([
        'id'   => '1',
        'name' => 'Taro',
    ], 200);
});
```

### Open Api Specの作成
仕様書はSwaggerHubを使用し作成。
```yaml:laravel-test-1.0.0-swagger.yaml
openapi: 3.0.0
servers:
  # Added by API Auto Mocking Plugin
  - description: SwaggerHub API Auto Mocking
    url: https://virtserver.swaggerhub.com/INDEX0205_1/laravel-test/1.0.0
  - description: Laravel OpenAPI Test
    url: aa
info:
  description: 「Laravel」と「OpenAPIの仕様書」間のテスト
  version: "1.0.0"
  title: Laravel OpenAPI Test
tags:
  - name: users
    description: ユーザー取得
paths:
  /api/user:
    get:
      tags:
        - users
      summary: searches user
      responses:
        '200':
          description: search results matching criteria
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  name:
                    type: string

```

### ライブラリを使用したテスト
```php:SampleControllerTest
<?php

declare(strict_types=1);

namespace Tests\Feature;

use GuzzleHttp\Psr7\Response;
use League\OpenAPIValidation\PSR7\ValidatorBuilder;
use Tests\TestCase;

class SampleControllerTest extends TestCase {
    public function test_assertion2(): void
    {
        $uri = '/api/user';

        // アサーション
        $response = $this->get($uri);
        $response
            ->assertOk()
            ->assertExactJson([
                'id' => '1',
                'name' => 'Taro',

        // バリデータの生成
        $validator = (new ValidatorBuilder)->fromYamlFile(__DIR__. '/../ApiSpec/laravel-test-1.0.0-swagger.yaml')->getResponseValidator();

        // APIを実行しレスポンスを取得
        $response = $this->json(
            'GET',
            $uri,
            [],
            []
        );

        // PSR-7準拠のレスポンスへ変換
        $psr7Response = new Response(
            $response->getStatusCode(),
            $response->headers->all(),
            $response->getContent(),
        );

        // 「仕様書のどの項目をテストするか」を変数に格納
        $address = new \League\OpenAPIValidation\PSR7\OperationAddress($uri, 'get');

        $validator->validate($address, $psr7Response);
    }
}

```
手順としては以下

1. バリデータを生成
2. APIを実行しレスポンスを取得。PSR-7準拠のレスポンスへ変換
    * 今回はGuzzleを使用。他にも方法はある
3. 仕様書中のどの項目をテストするか、の情報を変数に格納
4. バリデーション実行

※テスト内でassertOk()などのアサーションを実行しない場合、以下のように出力されるため注意
```
This test did not perform any assertions

C:\src\tests\Feature\SampleControllerTest.php:13

OK, but incomplete, skipped, or risky tests!
Tests: 2, Assertions: 1, Risky: 1.
```

# 実行してみる
問題なくテストが通った
```terminal
$ ./vendor/bin/phpunit
PHPUnit 9.5.24 #StandWithUkraine

..                                                                  2 / 2 (100%)

Time: 00:00.834, Memory: 22.00 MB

OK (2 tests, 3 assertions)
```

# わざとテストを失敗してみる
nameの型をintegerへ変更してみる
```yaml:laravel-test-1.0.0-swagger.yaml
openapi: 3.0.0
servers:
  # Added by API Auto Mocking Plugin
  - description: SwaggerHub API Auto Mocking
    url: https://virtserver.swaggerhub.com/INDEX0205_1/laravel-test/1.0.0
  - description: Laravel OpenAPI Test
    url: aa
info:
  description: 「Laravel」と「OpenAPIの仕様書」間のテスト
  version: "1.0.0"
  title: Laravel OpenAPI Test
tags:
  - name: users
    description: ユーザー取得
paths:
  /api/user:
    get:
      tags:
        - users
      summary: searches user
      responses:
        '200':
          description: search results matching criteria
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: string
                  name:
                    type: integer //変更

```
エラーが出力されたのでOK
```
$ ./vendor/bin/phpunit
PHPUnit 9.5.24 #StandWithUkraine

.E                                                                  2 / 2 (100%)

Time: 00:00.318, Memory: 22.00 MB

There was 1 error:

1) Tests\Feature\SampleControllerTest::test_assertion2
League\OpenAPIValidation\PSR7\Exception\Validation\InvalidBody: Body does not match schema for content-type "application/json" for Response [get /api/user 200]
```

# まとめ
[openapi-psr7-validator](https://github.com/thephpleague/openapi-psr7-validator)を使用し、「Laravelで実装したAPI」と「OpenAPIの定義ファイル」のテストを実行してみました。
ライブラリの使い方がREADMEを読んだだけではわかりにくい雑魚なので、参考記事を書いてくださる方には頭があがりません。

今後はSwaggerHubで仕様書を更新しプッシュ、Github Actionsでテストを自動実行、という流れを構築していきたいと思います。