16
15

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.

ApivoreでAPIの実装とOpenAPIドキュメントの乖離を防ぐ

Posted at

RESTfulなWeb APIについて、その実装とOpenAPIドキュメントの仕様記述が乖離すると、APIのユーザは仕様と実際の挙動が違うことに戸惑うかもしれませんし、Swagger Codegenなどのツールを使ってOpenAPIドキュメントを有効に活用することができません。

この問題をRailsで構築しているWeb APIで防ぐために、ApivoreとRSpecを使って、実装とドキュメントに差異が生じたときに検出できるようにします。ApivoreはAPIの実装とOpenAPIドキュメントのあいだでエンドポイント仕様が一致することをRSpecでチェックするためのgemです。

gemのインストール

Gemfile に次の行を追加して bundle install してください。

Gemfile
gem 'apivore'

本記事ではApivore 1.6.2を使います。

スペックの記述

準備

swagger.json の配信

テストで使うために、OpenAPIに則って書いた swagger.json をローカル環境でAPIから取得できるようにしておきます(public ディレクトリ配下にJSONを置く、swagger-blocksを使って動的に作成する、など)。つまり、次のように動作する状態にします。

$ curl https://localhost:3000/v1/swagger.json | jq .
{
  "swagger": "2.0",
  "info": {
    "description": "This is a sample server Petstore server.  You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/).  For this sample, you can use the api key `special-key` to test the authorization filters.",
    "version": "1.0.0",
    "title": "Swagger Petstore",
    "termsOfService": "http://swagger.io/terms/",
    "contact": {
      "email": "apiteam@swagger.io"
    },
    "license": {
      "name": "Apache 2.0",
      "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
    }
  },
  ...
}

Apivore::SwaggerChecker の生成

ApivoreをRSpecのテストで使うときは、オプションとして type: :apivore を渡します。

Apivore::SwaggerChecker をファクトリメソッド .instance_for で生成します。このとき、swagger.json を配信しているAPIのパスを引数として .instance_for に渡します。

spec/requests/api_spec.rb
describe 'API', type: :apivore do
  let(:checker) { Apivore::SwaggerChecker.instance_for('/v1/swagger.json') }
end

ドキュメントに対する実装のチェック

swagger.jsonGET /products/{productId} というエンドポイントの定義が次のとおり記述してあるとします。

swagger.json
{
  "paths": {
    "/products/{productId}": {
      "get": {
        "produces": [
          "application/json"
        ],
        "parameters": [
          {
            "name": "productId",
            "in": "path",
            "required": true,
            "type": "integer",
            "format": "int64"
          }
        ],
        "responses": {
          "200": {
            "schema": {
              "$ref": "#/definitions/Product"
            }
          }
        }
      }
    }
  },
  "definitions": {
    "Product": {
      "type": "object",
      "required": [
        "name"
      ],
      "properties": {
        "id": {
          "type": "integer",
          "format": "int64"
        },
        "name": {
          "type": "string"
        }
      }
    }
  }
}

このエンドポイントに対する実装とドキュメントの一致を見るテストは次にように書けます。

spec/requests/api_spec.rb
describe 'API', type: :apivore do
  let(:checker) { Apivore::SwaggerChecker.instance_for('/v1/swagger.json') }
  let(:product) { FactoryGirl.create(:product) } # factory_girlのフィクスチャが定義済みとする

  it { expect(checker).to validate(:get, '/products/{productId}', 200, { productId: product.id }) }
end

このテストは、GET /products/{productId} に対してリクエストを送り、次の項目をチェックしています。

  • GET /products/{productId} のエンドポイントが実際に存在すること
  • レスポンスのステータスコードが実装とドキュメントで一致していること
  • 実際のレスポンスJSONの形式がドキュメントに書かれたJSONスキーマと一致していること

これらのチェックの少なくとも一つがNGであればテストは失敗するので、APIの実装とドキュメントの乖離が防げます。

パラメータの指定

validate に対して、_query_string をキーとするHashとしてクエリパラーメタを渡せます。また、同様にリクエストボディ _data, リクエストヘッダ _headers も指定できます。

spec/requests/api_spec.rb
describe 'API', tpye: :apivore do
  let(:checker) { Apivore::SwaggerChecker.instance_for('/public/v1/swagger.json') }
  let(:params) {
    {
      _query_string: 'name=cup',
      _data: 'request body.',
      _headers: { 'Authentication': 'Basic XXXX' }
    }
  }
  
  it { expect(checker).to validate(:get, '/products.json', 200, params) }
end

なお、Apivoreは validate 実行時にクエリパラメータの形式がドキュメントと一致しているかについてはチェックしてくれません。

次のようにパラメータを作っておくと、あとで path_params, headers, query_params, data_params を上書きして値を変更しやすくなります。

let(:path_params) {{}}
let(:headers) {{}}
let(:query_params) {{}}
let(:data_params) {{}}
let(:params) {
  path_params.merge(
    '_headers' => headers,
    '_query_string' => query_params.to_query,
    '_data' => data_params
  )
}

全エンドポイントをテストしたことのチェック

Apivoreには、すべてのエンドポイントを validate でチェックしたかどうかをチェックする validate_all_paths というヘルパーも存在します。validate_all_paths はすべてのエンドポイントについて validate を実行したあとに実行したいので、テストがランダムに実行される場合を考えて、RSpecのオプション order: :defineddescribe に与えておきます。このオプションを指定すると、コードに書かれている順番にテストのexampleを実行します。

spec/requests/api_spec.rb
RSpec.describe 'API', type: :apivore, order: :defined do
  let(:checker) { Apivore::SwaggerChecker.instance_for('/v1/swagger.json') }
  let(:path_params) { {} }
  let(:headers) { {} }
  let(:query_params) { {} }
  let(:data_params) { {} }
  let(:params) {
    path_params.merge(
      '_headers' => headers,
      '_query_string' => query_params.to_query,
      '_data' => data_params
    )
  }

  context 'エンドポイントのテスト', order: :random do
    # 各エンドポイントのテストはランダム実行でもOK
    # shared_context に切り出しておくと見やすい
    include_context 'Product API'
  end

  it '全エンドポイントをテスト済み' do
    # このexampleは最後に実行したいので各エンドポイントのテストの後に書く
    expect(checker).to validate_all_paths
  end
end

テストしていないエンドポイントが検出されたときは次のようなエラーが表示されます。

1) 全エンドポイントをテスト済み
     Failure/Error: expect(checker).to validate_all_paths

       get /products/{productId} is untested for response code 404
       post /products is untested for response code 201
   ...   

参考

16
15
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
16
15

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?