44
24

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Laravelで実装したAPIがOpenAPIで記述された仕様に準拠しているかテストする

Posted at

はじめに

APIを開発する上で、多くの場合、仕様書も作成するかと思いますが、どのように作成しているでしょうか?
この記事では、OpenAPI形式で記述されたAPI仕様があって、その仕様にAPIの実装が準拠しているかテストする方法を紹介します。

tl;dr

OpenAPI v3とは(Swagger v2との違い)

詳しくは後述の参考リンクを参照してほしいのですが、誤解を恐れずに要点をまとめると以下のようになります。

  • APIを定義する仕様として、2010年にSwagger 1.0がリリースされる
  • Swaggerは、Open API Initiativeに寄贈され、2017-07-26にOpen API 3.0がリリースされる
  • Open API 3.0Swagger 2.0から大幅な変更が加えられており、互換性はない

参考リンク

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の検証はできます。

ポイントは以下のとおりです。

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.yamlOpenAPI v3形式のファイルを配置
  • OpenAPIValidation\PSR7\ValidatorBuilderfromYamlFilevalidatorを生成
  • PSR-7に準拠したRequest/Responseでなければいけないので、変換する必要がある
  • PSR-7形式への変換については、The PSR-7 Bridge (Symfony Docs)を参照
  • 仕様に違反した場合は、OpenAPIValidation\PSR7\Exception\ValidationFailedthrowされる
  • 今回はテストを失敗させるようにした

API仕様に違反すると

API仕様を変更して、テストを失敗させてみます。

任意項目だったNewPetおよびPettagという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を実装、保守しているサーバーサイドエンジニアの助けになれば幸いです。
ではでは。

44
24
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
44
24

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?