LaravelでOpenAPI/Swaggerを用いたFeatureテストをする


背景

開発したAPIがOpenAPI/Swagger通りになっているかをテストできれば最高ですよね.

https://nextat.co.jp/staff/archives/253

という素晴らしい記事があったのですが,コード量が多くなってしまうため少し手軽にできないかなっと考え探してみると,

https://github.com/hotmeteor/spectator

という良さげのを見つけたので試しに使ってみました.

環境

PHP 8.1.4
Laravel Framework 9.6.0

今回試したソースコード

https://github.com/katsuya-n/pub_laravel_spectator

OpenAPIを準備する

テストで使うためのopenapi.yamlを準備します.

$ mkdir ref/openapi.yaml
openapi.yaml
openapi: 3
info:
  title: Spectator Test API
  version: '1.0'
servers:
  - url: 'http://localhost'
paths:
  /api/status:
    get:
      description: Get
      responses:
        '500':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/res_api_status'
  /api/check:
    post:
      operationId: post-api-check
      description: Post
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/res_api_check'
    parameters: []
components:
  schemas:
    res_api_check:
      title: res_api_check
      type: object
      properties:
        date:
          type: object
          required:
            - year
            - month
            - day
          properties:
            year:
              type: integer
            month:
              type: integer
            day:
              type: integer
            timezone:
              type: string
        title:
          type: string
        level:
          type: string
          enum:
            - high
            - normal
            - low
        isLogin:
          type: boolean
      required:
        - date
        - title
        - level
        - isLogin
    res_api_status:
      title: res_api_status
      type: object
      properties:
        status:
          type: string
      required:
        - status

Stoplightのプレビューでみるとこんな感じです.POSTメソッドにしたり,ステータスコードが500になっているのは検証のためです.

hotmeteor/spectatorを入れる

公式のREAMDEにあるようにhotmeteor/spectatorを入れていきます.

$ composer require hotmeteor/spectator --dev
$ php artisan vendor:publish --provider="Spectator\SpectatorServiceProvider"

するとconfig/spectator.phpが作成されます.
読み込むOpenAPIはgithubの別リポジトリなど(remote, github)や同じアプリケーション上(local)が選択できるようです.
今回は同じアプリケーションにOpenAPIを作成したので,デフォルトのlocalのままでいきます.

2箇所だけ編集します.

config/spectator.php
    'sources' => [
        'local' => [
            'source' => 'local',
//            'base_path' => env('SPEC_PATH'),
            'base_path' => 'ref',
        ],
...
//    'suppress_errors' => false,
    'suppress_errors' => true,

base_pathにはOpenAPIのディレクトリを指定します.ちなみにアプリケーションの一番上の階層(app/やbootstrap/,config/などがある階層)を指定する場合は,''ではなく,'.'と指定しないと後のテストが通りませんでした.

suppress_errorsをtrueにすると,テストで失敗したときに詳細のエラー情報を出力してくれるのでtrueにしておきます.

/api/statusのテストを実行して,OpenAPIを変更してみる

レスポンスをコントローラーに書きます.

app/Http/Controllers/GetStatusController.php
...
        return response()->json(
            ['status' => 'OK'],
            500
        );
...

テストを書きます.

tests/Feature/GetStatusTest.php
<?php

namespace Tests\Feature;

use Spectator\Spectator;
use Tests\TestCase;

class GetStatusTest extends TestCase
{
    /**
     * @return void
     */
    public function testNormal()
    {
        Spectator::using('openapi.yaml');
        $this->get('/api/status')
            ->assertValidResponse(500);
    }
}
$ php artisan test tests/Feature/GetStatusTest.php

でテストが成功することを確認して,色々変更してみます.

例えば,次のようにOpenAPIのレスポンスstatusを val に変更して,テストを実行してみます.

ref/openapi.yaml
...
    res_api_status:
      title: res_api_status
      type: object
      properties:
        val:
          type: string
      required:
        - val
...
$ php artisan test tests/Feature/GetStatusTest.php
...
The required properties (val) are missing

object++ <== The required properties (val) are missing
    val*: string
Failed asserting that true is false.
...

と必須項目valがないよ!というエラーになりました.
今度はstatusに戻して,型をstringから integer に変更してみます.

ref/openapi.yaml
...
    res_api_status:
      title: res_api_status
      type: object
      properties:
        status:
          type:
            - integer
      required:
        - status
...
$ php artisan test tests/Feature/GetStatusTest.php
...
The properties must match schema: status
The data (string) must match the type: integer

object++ <== The properties must match schema: status
    status*: integer <== The data (string) must match the type: integer
...

とstringじゃなくてintegerだよ!というエラーになりました.

/api/checkのテストを実行して,APIレスポンスを変更してみる

レスポンスをコントローラーに書きます.

app/Http/Controllers/PostCheckController.php
        return response()->json([
            'date' => [
                'year' => 2022,
                'month' => 4,
                'day' => 6,
            ],
            'title' => 'test response!',
            'level' => 'high',
            'isLogin' => false
        ],
            200
        );
...

テストを書きます.

tests/Feature/PostCheckTest.php
<?php

namespace Tests\Feature;

use Spectator\Spectator;
use Tests\TestCase;

class PostCheckTest extends TestCase
{
    /**
     * @return void
     */
    public function testNormal()
    {
        Spectator::using('openapi.yaml');
        $this->post('/api/check')
            ->assertValidResponse(200);
    }
}

テストが成功することを確認して,今度はAPIレスポンス側を色々変更してみます.

コントローラーのレスポンスlevelを'high'から true に変更してみると

app/Http/Controllers/PostCheckController.php
...
//            'level' => 'high',
            'level' => true,
...
$ php artisan test tests/Feature/PostCheckTest.php 
The properties must match schema: level
The data (boolean) must match the type: string

object++ <== The properties must match schema: level
    date*: object++
        year*: integer
        month*: integer
        day*: integer
        timezone: string
    title*: string
    level*: string [high, normal, low] <== The data (boolean) must match the type: string
    isLogin*: boolean
Failed asserting that true is false.

と型がboolじゃなくてstringだよ!というエラーになりました.
今度は 'hoge' としてみると

app/Http/Controllers/PostCheckController.php
...
//            'level' => 'high',
            'level' => 'hoge',
...
$ php artisan test tests/Feature/PostCheckTest.php 
The properties must match schema: level
The data should match one item from enum

object++ <== The properties must match schema: level
    date*: object++
        year*: integer
        month*: integer
        day*: integer
        timezone: string
    title*: string
    level*: string [high, normal, low] <== The data should match one item from enum
    isLogin*: boolean
Failed asserting that true is false.

と型がenumと一致してないよ!というエラーになりました.

まとめ

コード記述量も少ないし,インストールも難しくなくvery goodです.もう少し深堀りして使ってみると不具合が発見できるかもしれませんが,ざっと使ってみた感じは問題なさそうでした.