LaravelでOpenAPI/Swaggerを用いたFeatureテストを行う
背景
開発したAPIがOpenAPI/Swagger通りになっているかをテストできれば最高ですよね.
という素晴らしい記事があったのですが,コード量が多くなってしまうため少し手軽にできないかなっと考え探してみると,
という良さげのを見つけたので試しに使ってみました.
環境
PHP 8.1.4
Laravel Framework 9.6.0
今回試したソースコード
OpenAPIを準備する
テストで使うためのopenapi.yamlを準備します.
$ mkdir ref/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箇所だけ編集します.
'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を変更してみる
レスポンスをコントローラに書きます.
...
return response()->json(
['status' => 'OK'],
500
);
...
テストを書きます.
<?php
namespace Tests\Feature;
use Spectator\Spectator;
use Tests\TestCase;
class GetStatusTest extends TestCase
{
/**
* @return void
*/
public function testNormal()
{
Spectator::using('openapi.yaml');
$this->getJson('/api/status')
->assertValidResponse(500);
}
}
$ php artisan test tests/Feature/GetStatusTest.php
でテストが成功することを確認して,色々変更してみます.
例えば,次のようにOpenAPIのレスポンスstatusを val に変更して,テストを実行してみます.
...
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 に変更してみます.
...
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レスポンスを変更してみる
レスポンスをコントローラに書きます.
return response()->json([
'date' => [
'year' => 2022,
'month' => 4,
'day' => 6,
],
'title' => 'test response!',
'level' => 'high',
'isLogin' => false
],
200
);
...
テストを書きます.
<?php
namespace Tests\Feature;
use Spectator\Spectator;
use Tests\TestCase;
class PostCheckTest extends TestCase
{
/**
* @return void
*/
public function testNormal()
{
Spectator::using('openapi.yaml');
$this->postJson('/api/check')
->assertValidResponse(200);
}
}
テストが成功することを確認して,今度はAPIレスポンス側を色々変更してみます.
コントローラのレスポンスlevelを'high'から true に変更してみると
...
// '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' としてみると
...
// '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です.もう少し深堀りして使ってみると不具合が発見できるかもしれませんが,ざっと使ってみた感じは問題なさそうでした.
Discussion