APIドキュメントに書いたJSON Schemaと実際に実装したWeb APIのレスポンスJSONが一致するかバリデーションするためのCommitteeというgemがあります。また、このCommitteeをRailsプロジェクト中のテストから使うためのCommittee::Railsというgemがあります。
CommitteeはAPIドキュメントの形式としてJSON Hyper SchemaとOpenAPI 2.0に対応しています。また、APIエンドポイントを叩いたときのレスポンスJSONがドキュメントで定義したJSON Schemaと一致したかを確認するアサーションメソッド assert_schema_conform
を持っているので、このメソッドを使ってAPIドキュメントの実際の動作の乖離を未然に防ぐことができます。
今回はOpenAPI 2.0の形式で書いたAPIドキュメントを使って、Railsで作ったAPIのエンドポイントからのレスポンスをRspecのテストでバリデーションしてみます。
使用するライブラリのバージョン
ライブラリのバージョンは次のものとします。
- Committee 2.0.0
- Committee::Rails 0.2.0
例のAPI仕様
今回、次のようなAPIエンドポイントを持つ単純なアプリケーションを考えます。
このエンドポイントはステータスコード200で userId
のIDを持つユーザを返します。ここで、ユーザは次のような属性を持つデータとします。
属性名 |
必須 |
id |
○ |
email |
○ |
name |
○ |
age |
|
すなわち、レスポンスのJSONは次のような形となります。
{
"id": 1,
"email": "foo@example.com",
"name": "John Doe",
"age": 25
}
OpenAPIドキュメントの記述
上述した仕様に基づいて、次のようなOpenAPI 2.0形式のドキュメントを書きます。
{
"swagger": "2.0",
"info": {
"version": "1.0.0",
"title": "Committee Rails Sample",
"license": {
"name": "MIT"
}
},
"host": "example.com",
"schemes": [
"http"
],
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {
"/users/{userId}": {
"get": {
"summary": "get users",
"operationId": "userShow",
"tags": [
"users"
],
"parameters": [
{
"name": "userId",
"in": "path",
"description": "user ID",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "A user",
"schema": {
"$ref": "#/definitions/User"
}
}
}
}
}
},
"definitions": {
"User": {
"required": [
"id",
"email",
"name"
],
"properties": {
"id": {
"type": "integer"
},
"email": {
"type": "string"
},
"name": {
"type": "string"
},
"age": {
"type": "integer"
}
},
"additionalProperties": false
}
}
}
ここでは次の点に注目してもらえればOKです。
GET /usres/{userId}
のレスポンスとして definitions
配下のデータ定義 User
を使っている
- データ定義
User
では required
で必須パラメータを指定しつつ、additionalProperties
に false
を指定して記述したパラメータ以外が含まれることを禁じている
Committee::RailsでOpenAPIを使う準備
Committee::RailsでOpenAPIを使うために、Committee::Test::Methods#committee_schema
というメソッドをオーバーライドします。このメソッドはAPIドキュメントに書いたJSON Schemaで実際のJSONをバリデーションするときに、そのAPIドキュメントを読み込むメソッドです。Committee::Railsでは Committee::Rails::Test::Methods#commitee_schema
でJSON Hyper Schemaのドライバを使うようにあらかじめオーバーライドしていますが、今回はOpenAPI 2.0のドライバを使いたいので、自前でオーバーライドし直します。
module CommittteeRailsOpenapi2
include Committee::Rails::Test::Methods
def committee_schema
@committee_schema ||=
begin
driver = Committee::Drivers::OpenAPI2.new
schema_hash = JSON.parse(File.read(schema_path))
driver.parse(schema_hash)
end
end
def schema_path
Rails.root.join('docs', 'swagger.json')
end
end
ここでは、例としてRailsプロジェクトの docs/swagger.json
に存在するOpenAPIドキュメントを読み込んでいます。
テストの記述
ここまで来ると、上述の committee_schema
オーバーライドによって、APIエンドポイントが返すレスポンスがOpenAPIドキュメントに記述したJSON Schemaに一致するかどうかをRailsのテストで確認できるようになりました。テストはCommittee::RailsのREADMEに書かれているものとまったく同じで、RSpecを使うと次のように書けます。
require 'rails_helper'
RSpec.describe 'Users', type: :request do
describe 'GET /users/:id' do
let!(:user) { create(:user) }
it 'レスポンスがAPI定義と一致する' do
get "/users/#{user.id}"
assert_schema_conform
end
end
end
もしレスポンス用テンプレートの記述を間違えて必須属性 email
を含めなかった場合、次のようなエラーが出ます。
1) Users GET /users/:id レスポンスがAPI定義と一致する
Failure/Error: assert_schema_conform
Committee::InvalidResponse:
Invalid response.
#: failed schema #/properties//users/{userId}/properties/GET: "email" wasn't supplied.
また、もしレスポンス用テンプレートの記述を間違えてOpenAPIドキュメントで定義していない属性 phone
を含めた場合、次のようなエラーが出ます。
1) Users GET /users/:id レスポンスがAPI定義と一致する
Failure/Error: assert_schema_conform
Committee::InvalidResponse:
Invalid response.
#: failed schema #/properties//users/{userId}/properties/GET: "phone" is not a permitted key.
さらに、 属性 age
は必須としない定義にしているので、レスポンスに age
を含めなくてもエラーにはなりません。
なお、注意点として、ライブラリの実装上、正常系(ステータスコード200〜300番台)のレスポンスだけテストでき、異常系(ステータスコード400〜500番台)についてはテストできません*1。
まとめ
- Railsで作るWeb APIのレスポンスJSONがOpenAPIに定義したJSON Schemaと一致しているかをチェックするにはCommitteeとCommittee::Railsを使う
- OpenAPI 2.0の形式のドキュメントを読み込むように必要なメソッドをオーバーライドしておく必要がある