はじめに
APIを開発する上で、多くの場合、仕様書も作成するかと思いますが、どのように作成しているでしょうか?
この記事では、OpenAPI形式で記述されたAPI仕様があって、その仕様にAPIの実装が準拠しているかテストする方法を紹介します。
tl;dr
-
thephpleague/openapi-psr7-validatorを使って、
Request
およびResponse
がAPI仕様に準拠しているか検証する -
実行可能なサンプルを用意したので、
git clone
して色々試してほしい
OpenAPI v3
とは(Swagger v2
との違い)
詳しくは後述の参考リンクを参照してほしいのですが、誤解を恐れずに要点をまとめると以下のようになります。
- APIを定義する仕様として、2010年に
Swagger 1.0
がリリースされる -
Swagger
は、Open API Initiative
に寄贈され、2017-07-26にOpen API 3.0
がリリースされる -
Open API 3.0
はSwagger 2.0
から大幅な変更が加えられており、互換性はない
参考リンク
- https://en.wikipedia.org/wiki/OpenAPI_Specification
- https://swagger.io/docs/specification/about/
- https://news.mynavi.jp/itsearch/article/devsoft/3854
OpenAPI v3
に対応した検証ライブラリは多くない(2019年10月1日現在)
Swagger v2
に対応したPHP製の検証ライブラリはいくつかあります。
しかし、いずれも、OpenAPI v3
には未対応です。
WakeOnWeb/swagger
OpenAPI v3 support · Issue #16 · WakeOnWeb/swaggerはいまだにOpen
のままです。
nabbar/SwaggerValidator-PHP
README.mdを見ると、swagger / openapi version 3.0 (release >= 2.0)
とあるので、v2.0以上だったらOpen API 3.0
に対応しているのかと思いきや、最新のバージョンは1.3.2
です。
対応予定だけ書いて力尽きたようです。
OpenAPI v3
形式のファイルをJSON Schema
に変換すれば、検証はできる
laravel-petstore-apiの最初のコミットでもこの方法を使っているのですが、OpenAPI v3
形式のファイルをJSON Schema
に変換し、Request
およびResponse
の検証はできます。
ポイントは以下のとおりです。
-
openapi2schemaを使って、
OpenAPI v3
からJSON Schema
へ変換する -
justinrainbow/json-schemaを使って、
Request
およびResponse
を検証する
2019年9月 thephpleague/openapi-psr7-validator
がリリースされる
この記事を書いている途中で気付いてしまったのですが、thephpleague/openapi-psr7-validator
という何とも期待の持てるライブラリがあったので、試してみました。
結果、期待どおり動いたので、それまでのJSON Schema
に変換してから検証する実装、関連ライブラリを全部捨ててthephpleague/openapi-psr7-validator
で実装し直しました。
実装
実装は下記のようになりました。
<?php
namespace Tests\Feature;
use Illuminate\Foundation\Testing\TestResponse;
use Nyholm\Psr7\Factory\Psr17Factory;
use OpenAPIValidation\PSR7\Exception\ValidationFailed;
use OpenAPIValidation\PSR7\OperationAddress;
use OpenAPIValidation\PSR7\ValidatorBuilder;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
use Tests\TestCase;
/**
* Trait AssertResponseCompliantForSwaggerApiSpec
* @package Tests\Feature
*
* @mixin TestCase
*/
trait OpenApiSpecAssertions
{
/**
* @param TestResponse $testResponse
* @param string $method
* @param string $path
*/
protected function assertResponseCompliantForOpenApiSpec(
TestResponse $testResponse,
string $method,
string $path
) {
$validator = (new ValidatorBuilder())
->fromYamlFile(__DIR__. '/../ApiSpec/petstore-expanded.yaml')
->getResponseValidator()
;
$operation = new OperationAddress($path, strtolower($method)) ;
$psr17Factory = new Psr17Factory();
$psrHttpFactory = new PsrHttpFactory($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory);
$psrResponse = $psrHttpFactory->createResponse($testResponse->baseResponse);
try {
$validator->validate($operation, $psrResponse);
} catch (ValidationFailed $validationFailed) {
self::fail($validationFailed->getPrevious()->getMessage());
}
}
/**
* @param array $requestBody
* @param string $method
* @param string $uri
*/
protected function assertRequestCompliantForOpenApiSpec(
array $requestBody,
string $method,
string $uri
) {
$validator = (new ValidatorBuilder())
->fromYamlFile(__DIR__. '/../ApiSpec/petstore-expanded.yaml')
->getRequestValidator()
;
$psr17Factory = new Psr17Factory();
$json = json_encode($requestBody, JSON_UNESCAPED_UNICODE);
$stream = $psr17Factory->createStream($json);
$stream->rewind();
$request = $psr17Factory->createRequest(strtolower($method), $uri)
->withBody($stream)
->withHeader('Content-Type', 'application/json')
;
try {
$validator->validate($request);
} catch (ValidationFailed $validationFailed) {
self::fail($validationFailed->getPrevious()->getMessage());
}
}
}
ポイント
-
tests/ApiSpec/petstore-expanded.yaml
にOpenAPI v3
形式のファイルを配置 -
OpenAPIValidation\PSR7\ValidatorBuilder
のfromYamlFile
でvalidator
を生成 -
PSR-7に準拠した
Request
/Response
でなければいけないので、変換する必要がある -
PSR-7
形式への変換については、The PSR-7 Bridge (Symfony Docs)を参照 - 仕様に違反した場合は、
OpenAPIValidation\PSR7\Exception\ValidationFailed
がthrow
される - 今回はテストを失敗させるようにした
API仕様に違反すると
API仕様を変更して、テストを失敗させてみます。
任意項目だったNewPet
およびPet
のtag
というproperty
を必須(required
)にして、テストを実行します。
openapi: "3.0.0"
## 中略 ##
components:
schemas:
Pet:
allOf:
- $ref: '#/components/schemas/NewPet'
- type: object
required:
- id
properties:
id:
type: integer
format: int64
NewPet:
type: object
required:
- name
- tag ## この行を追加
properties:
name:
type: string
tag:
type: string
Keyword validation failed: Required property 'tag' must be present in the object
というメッセージとともに、テストが失敗します。
$ vendor/bin/phpunit
PHPUnit 8.3.5 by Sebastian Bergmann and contributors.
..FFF... 8 / 8 (100%)
Time: 405 ms, Memory: 24.00 MB
There were 3 failures:
1) Tests\Feature\Pet\IndexTest::indexSuccess
Keyword validation failed: Required property 'tag' must be present in the object
/path/to/laravel-petstore-api/tests/Feature/OpenApiSpecAssertions.php:45
/path/to/laravel-petstore-api/tests/Feature/Pet/IndexTest.php:23
2) Tests\Feature\Pet\ShowTest::showSuccess
Keyword validation failed: Required property 'tag' must be present in the object
/path/to/laravel-petstore-api/tests/Feature/OpenApiSpecAssertions.php:45
/path/to/laravel-petstore-api/tests/Feature/Pet/ShowTest.php:24
3) Tests\Feature\Pet\StoreTest::storeSuccess
Keyword validation failed: Required property 'tag' must be present in the object
/path/to/laravel-petstore-api/tests/Feature/OpenApiSpecAssertions.php:76
/path/to/laravel-petstore-api/tests/Feature/OpenApiSpecAssertions.php:98
/path/to/laravel-petstore-api/tests/Feature/Pet/StoreTest.php:23
FAILURES!
Tests: 8, Assertions: 13, Failures: 3.
おわりに
API仕様に準拠しているか、実装が変わるたびに検証されていないと、しだいにAPI仕様が陳腐化し、結果、「今動いている実装が正しい」ということになりがちです。
もちろん、動いているAPIから仕様書を自動生成するというアプローチもありますが、以下のような問題があると思います。
- API仕様を変更するのに
PHP
を触らなければいけない - API仕様を
PHP
のアノテーションで定義する形式だと、検査されずに陳腐化する可能性がある
なので、先にAPI仕様をOpenAPI v3
形式で作成してから、クライアントチームと合意をとりながら開発を進めていくのが良いのかなと個人的には考えています。
この記事が、日々、多くのAPIを実装、保守しているサーバーサイドエンジニアの助けになれば幸いです。
ではでは。