はじめに
VASILYでは、API開発を始める前に、Swaggerを用いてAPIドキュメントを作成しています。
APIドキュメントを作成した後、実際のAPIレスポンスを修正したがドキュメントの更新を忘れ、ドキュメントの定義と実際のレスポンスの間に差異が生じてしまうということがありました。
そこで、今回はcommitteeというgemを用いて、Swaggerで定義したAPIドキュメントと実際のAPIレスポンスとの差異を検知する方法をご紹介します。
committeeとは
committeeは、実際のAPIリクエストやレスポンスがスキーマ定義にそっているかをチェックすることができるgemです。
Rackのミドルウェアとして動作します。
バージョン2.0からはJSON Schemaだけでなく、OpenAPI2.0(Swagger)もサポートされるようになったため今回はこちらのgemを使用します。
committeeのインストール
Gemfileに追記します。
gem 'committee'
APIドキュメンテーションを用意する
Swaggerを書く
SwaggerでAPIドキュメントを記述します。
Swaggerの導入方法、書き方については、下記の記事を参考にして下さい。
SwaggerはYAMLとJSONで書くことができます。
今回は、YAMLを採用し、公式ドキュメントでサンプルとして使われているPetstoreAPIを簡略化したものをサンプルAPIドキュメントとして用います。
# pet.yaml
swagger: "2.0"
info:
description: "これはペットストアに関するAPIです。"
version: "1.0.0"
title: "Petstore API"
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"
produces:
- "application/json"
consumes:
- application/x-www-form-urlencoded
paths:
/pets/{petId}:
get:
summary: "ペット情報API"
description: "指定されたpetIdの情報を返します"
operationId: 'get_pet_by_id'
tags:
- 'pet'
parameters:
- name: "petId"
in: "path"
description: "取得したいペットのID"
required: true
type: "integer"
responses:
200:
description: "成功時のレスポンス"
schema:
$ref: '#/definitions/Pet'
definitions:
Pet:
type: object
required:
- id
- name
properties:
id:
type: "integer"
name:
type: "string"
example: "doggie"
YAMLをJSONに変換
YAMLで書いたSwaggerをJSONに変換します。
変換には、swagger-codegenを使用します。
swagger-codegenは、Swaggerで書かれたスキーマ定義からクライアントやサーバーのコードを生成するツールです。
多くの言語やフレームワークに対応しており、Rubyのモックコードも生成できます。
swagger-codegenを使用してYAMLをJSONに変換します。
今回はMacのローカルで生成を行うのでhomebrewでswagger-codegenを入れます。
# swagger-codegenをインストール
% brew install swagger-codegen
# yamlからjsonへ変換
% swagger-codegen generate -i pet.yaml -l swagger
作成されたJSONが下記になります。
{
"swagger" : "2.0",
"info" : {
"description" : "これはペットストアに関するAPIです。",
"version" : "1.0.0",
"title" : "Petstore API",
"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"
}
},
"consumes" : [ "application/x-www-form-urlencoded" ],
"produces" : [ "application/json" ],
"paths" : {
"/pets/{petId}" : {
"get" : {
"tags" : [ "pet" ],
"summary" : "ペット情報API",
"description" : "指定されたpetIdの情報を返します",
"operationId" : "get_pet_by_id",
"parameters" : [ {
"name" : "petId",
"in" : "path",
"description" : "取得したいペットのID",
"required" : true,
"type" : "integer"
} ],
"responses" : {
"200" : {
"description" : "成功時のレスポンス",
"schema" : {
"$ref" : "#/definitions/Pet"
}
}
}
}
}
},
"definitions" : {
"Pet" : {
"type" : "object",
"required" : [ "id", "name" ],
"properties" : {
"id" : {
"type" : "integer"
},
"name" : {
"type" : "string",
"example" : "doggie"
}
}
}
}
}
request specを書く
APIドキュメントにもとづいたAPIを作成します。
今回はわかりやすくするために、swagger-codegenを使ってモックコードを生成せずに、手動でRailsのコードを作成します。
config/routes.rb に/pets/#{pet_id}
のルーティングを追加し、JSONを返すだけのControllerを作成します。
# config/routes.rb
resources :pets, only: [:show]
# app/controllers/pets_controller.rb
class PetsController < ApplicationController
def show
render json: { id: 1, name: 'pochi' }
end
end
APIのレスポンスをテストするために、request specを書きます。
request specは、クライアント側の動作や振る舞い、特定のリクエストに対するHTTPレスポンスをテストします。
詳しい説明については、rspecのREADMEを参考にしてください。
PetstoreAPIのrequest specを作成します。
% bundle exec rails g rspec:integration Pet
上記のコマンドを実行すると、下記のようなspecが生成されます。
# spec/requests/pets_spec.rb
require 'rails_helper'
RSpec.describe "Pets", type: :request do
describe "GET /pets" do
it "works! (now write some real specs)" do
get pets_path
expect(response).to have_http_status(200)
end
end
end
このrequest specに、committeeの設定を追記します。
今回はわかりやすさを重視して、specファイル内にcommitteeの設定を書きます。
committeeの設定を追加すると下記のようになります。
# spec/requests/pet_spec.rb
require 'rails_helper'
RSpec.describe "Pets", type: :request do
include Committee::Test::Methods
include Rack::Test::Methods
def committee_schema
@committee_schema ||=
begin
driver = Committee::Drivers::OpenAPI2.new
schema = JSON.parse(File.read(schema_path))
driver.parse(schema)
end
end
def schema_path
Rails.root.join('swagger.json')
end
describe "GET /pets", type: :request do
it "レスポンスがAPIドキュメントと一致する" do
get '/pets/1'
assert_schema_conform
end
end
end
schema_pathメソッドとcommittee_schemaメソッドを追加しました。
どちらも、committeeのメソッドをオーバーライドしています。
schema_pathは、API定義のJSONが置かれているパスを記述します。
committeeのschema_pathは、使用する際にオーバライドする必要があり、オーバーライドしないとエラーが返ります。
schema_path
committee_schemaは、OpenAPI Specをパースするためにオーバーライドします。
このメソッドはAPIドキュメントを読み込む際に使用されます。
driverを指定しないと、JSON Hyper Schemaのドライバが使われます。
今回は、Swaggerで定義したOpenAPI Specをパースするために、OpenAPIのドライバを使用したいので、committee_schemaメソッドをオーバーライドします。
committee_schema
def committee_schema
@committee_schema ||=
begin
driver = Committee::Drivers::OpenAPI2.new
schema = JSON.parse(File.read(schema_path))
driver.parse(schema)
end
end
テスト内にassert_schema_confirm
を記述することで、APIドキュメントと実際のレスポンスが、一致しているかどうかのテストが実行されます。
APIドキュメント内で、requiredに記述した必須カラムを記述していない場合は、下記のようなエラーが表示されます。
1) Pets GET /pets レスポンスがAPIドキュメントと一致する
Failure/Error: assert_schema_conform
Committee::InvalidResponse:
Invalid response.
#: failed schema #/properties//pets/{petId}/properties/GET: "name" wasn't supplied.
# ./spec/requests/pets_spec.rb:22:in `block (3 levels) in <top (required)>'
最後に
Swaggerと実際のAPIレスポンスを、人が都度修正する運用は辛いですし、どうしても差異が発生しやすくなります。
committeeとrequest specを用いて、ドキュメントとの差異を防いでいきたいです。
参考
スキーマファースト開発のススメ
Web APIのレスポンスJSONをCommittee + OpenAPIでバリデーションして仕様と実装の乖離を防ぐ
JSON Validation by Committee