LaravelでAPI仕様書(OAS)と実装の乖離を簡単に防ぐならSpectatorがおすすめ！

皆さんはAPI仕様書(OAS)と実装の乖離を防ぐためにどのような対応をされていますでしょうか？

- コードからOASを自動生成することで乖離を防ぐ
- OASからコードを自動生成することで乖離を防ぐ
- PRレビュー時に目視で乖離を防ぐ
- 自作の検証ツールで乖離を防ぐ

などなどいくつかのアプローチが考えられるのかなと思います。

良い感じに自動生成ができれば乖離の心配はないかもしれませんが、少なからず手動作業が入る場合は何らか乖離を防ぐ検証が必要になると思います。

今回は、Laravelでそんなシーンに出くわした際にぜひ検討いただきたい [Spectator](https://github.com/hotmeteor/spectator) というツールを紹介させていただきます。

# 前提

php: 8.1.6
laravel/framework: 9.17.0
hotmeteor/spectator: 1.5.0
サンプルコード: https://github.com/TsukasaGR/laravel-spectator-sample

# Spectator is 何？

READMEには

> Spectator provides light-weight OpenAPI testing tools you can use within your existing Laravel test suite.
>  
> Write tests that verify your API spec doesn't drift from your implementation.

とあります。

ざっくり言うと、Laravelのテストの仕組みを利用して簡単にAPI仕様書(OAS)が実装から逸脱していないかを確認してくれるツールです。

# 使い方

## 導入

まずは[README](https://github.com/hotmeteor/spectator#installation)の通り

```bash
# パッケージインストール
composer require hotmeteor/spectator --dev

# 設定ファイルを外に出す　
php artisan vendor:publish --provider="Spectator\SpectatorServiceProvider"
```

を行います。

次に、上記で生成された `config/spectator.php` をご自身の環境に合わせて修正します。

デフォルトはローカルにあるymlを読み込むようになっていますので、ローカルにymlがある場合は

```:config/spectator.php
// 省略
    'sources' => [
        'local' => [
            'source' => 'local',
            'base_path' => env('SPEC_PATH'),
        ],
// 省略
```

上記localのbase_pathを適宜修正します。
例えばルートディレクトリに配置している場合は

```:.env
SPEC_PATH=.
```

とすればOKです。

## テスト

### 実装方法

こちらも[README](https://github.com/hotmeteor/spectator#testing)に記載されている通りですが、例えば[こちら](https://github.com/TsukasaGR/laravel-spectator-sample/blob/main/openapi.yml)のようなOASの検証を行う場合は以下のようなテストを記載すればOKです。

```php:test/Feature/SampleTest.php
<?php

namespace Tests\Feature;

use Spectator\Spectator;
use Tests\TestCase;

class SampleTest extends TestCase
{
    /**
     * /api/sampleのGETメソッドのレスポンス200を検証するテスト
     */
    public function test_spec_get_sample_200()
    {
        Spectator::using('openapi.yml');
        $id = 1;
        $this->getJson("/api/sample/${id}")
            ->assertValidRequest()
            ->assertValidResponse(200);
        }

    /**
     * /api/sampleのPOSTメソッドのレスポンス200を検証するテスト
     */
    public function test_spec_post_sample_200()
    {
        Spectator::using('openapi.yml');
        $this->postJson('/api/sample', ['id' => 1])
            ->assertValidRequest()
            ->assertValidResponse(200);
    }
}
```

念の為少しだけ説明も加えておきます。

まずは

```php
Spectator::using('openapi.yml');
```

で対象のAPI仕様書の場所を定義することで検証が可能になります。

続いて、

```php
$this->getJson("/api/sample/${id}")
    ->assertValidRequest()
    ->assertValidResponse(200);
```

xxxJson(ex. getJson, postJson)メソッドでAPIを実行し、

- `assertValidRequest` でリクエストパラーメーターを検証
- `assertValidResponse` でレスポンスパラメーターを検証

します。

### 乖離があった場合どのような結果になるか

例えば、API仕様書が以下(レスポンスにidとstatusが必須)であり、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173370/b6e29c7f-fb7c-6e6e-c8ae-657a053b1cd3.png)

実装が以下(idは返すがstatusは返さない)のような場合、
```php:routes/api.php
Route::post('sample', function (Request $request) {
    return response()->json([
        'id' => (int)$request->id,
    ]);
});
```

以下のように `必須のプロパティstatusがミスってます` とエラーを吐いてくれます。

```bash
  ---                                                        
                                                             
  The required properties (status) are missing               
                                                             
  object++ <== The required properties (status) are missing  
      id*: number                                            
      status*: string                                        
 =========================================================== 
  ⨯ The required properties (status) are missing             
 =========================================================== 
 =========================================================== 
```

存在有無だけでなく、number ⇔ stringの型の差異などもチェックしてくれます。

# 何が嬉しいのか

ここまでの情報で十分かもしれませんが、念の為私が感じている具体的な嬉しい点もいくつか挙げさせていただきます。

## 1. 初期導入コストが低い

`Spectator::using` + `assertValidRequest / Response` の数行だけで検証ができるため、新規プロダクトはもちろん、運用中のプロダクトに途中から導入することも非常に簡単です。

## 2. メンテナンスコストも低い

一度テストを書いてしまえば、少なくとも `レスポンスの変更` に対してテストコードを変更する必要がありません。
リクエストパラメーターの変更ではテストコードの修正が必要になりますが、それでもメンテナンスコストは非常に低いと思います。

## 3. コスパが良い

1.2.の通りコストは低いですが、それに対するリターンは非常に大きいと思います。
仕様書との乖離だけでなく、例えば

```php
public function test_post_sample_200()
{
    $this->postJson('/api/sample', ['id' => 1])
        ->assertStatus(200);
}
```

のような `そもそも正常に動作しているか` や `きちんとバリデーションエラーが返されるか` などの最低限のテストにもなってくれます。

そのため、 `仕様書との乖離以前にテストを全く書けてない。。。` という悩みをお持ちのプロダクトにも適用しやすいと思います。

# Tips(詰まったことなど)

導入は簡単でしたが、いくつか詰まることもあったので挙げておきます。

## 1. api用のミドルウェアを噛ませないと検証できない

`APIなんだからAPIのミドルウェアを噛ませるのは当然だろ` というツッコミもあると思いますが、例えば[Laravel Sanctum](https://laravel.com/docs/9.x/sanctum)を導入するとデフォルトではapi.phpでなくweb.php側にルーティングが定義されるため、Laravel Santumによる認証を検証したい場合は

- api.phpにルートを移動する
- 個別にapi用ミドルウェアを噛ませる

などのひと手間が必要です。

## 2. pathはstringでないといけない

以下のようなエンドポイントの場合、

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/173370/ce6a90a4-b3a8-24b7-13e5-d00930a3f651.png)

仕様書上のidの型をnumberにしてしまうとうまく検証できないため、pathパラメーターはstringにする必要があります。

# 参考にさせていただいた情報

以下の記事でSpectatorの存在を知ることができました。
また、実装例などは本記事よりも以下記事のほうが丁寧に説明されています。
ぜひご覧いただければと思います。

https://zenn.dev/lightkun/articles/5423cb62f118ba