Kōhei Yamamoto

Web APIのレスポンスJSONをCommittee + OpenAPIでバリデーションして仕様と実装の乖離を防ぐ

APIドキュメントに書いたJSON Schemaと実際に実装したWeb APIのレスポンスJSONが一致するかバリデーションするためのCommitteeというgemがあります。また、このCommitteeをRailsプロジェクト中のテストから使うためのCommittee::Railsというgemがあります。

CommitteeはAPIドキュメントの形式としてJSON Hyper SchemaOpenAPI 2.0に対応しています。また、APIエンドポイントを叩いたときのレスポンスJSONがドキュメントで定義したJSON Schemaと一致したかを確認するアサーションメソッド assert_schema_conform を持っているので、このメソッドを使ってAPIドキュメントの実際の動作の乖離を未然に防ぐことができます。

今回はOpenAPI 2.0の形式で書いたAPIドキュメントを使って、Railsで作ったAPIのエンドポイントからのレスポンスをRspecのテストでバリデーションしてみます。

使用するライブラリのバージョン

ライブラリのバージョンは次のものとします。

例の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です。

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のドライバを使いたいので、自前でオーバーライドし直します。

spec/support/committee_rails_openapi2.rb
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を使うと次のように書けます。

spec/requests/users_spec.rb
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

まとめ

脚注

  1. Committee::Rails::Test::Methods#assert_schema_conformCommittee::ResponseValidator.validate? を参照