REST APIを開発するにあたり、OpenAPI Specification(OpenAPI 3.0以前でいうところのSwagger Specification)をAPIの仕様書として利用している方は多いのではないでしょうか。
しかし、OpenAPI Specificationを仕様書として利用する場合、きちんとメンテナンスを続けなければ実際のAPIの挙動と仕様書の内容に違いが生まれてきてしまいます。
committeeを利用することで、OpenAPI Specificationと実際のAPIの挙動が一致しているかテストコードで検証できます。
今回はcommitteeの導入手順と、実際にテストコードでcommitteeを利用する方法について紹介します。
committee-railsは0.5.1
、committeeは4.2.1
を利用します。
目次
今回の検証環境とサンプルAPIについて
今回は「API開発 + Swagger UIを利用したAPI検証」な環境をDockerで構築するで紹介した、以下のようなDocker環境で検証を行ます。

検証用のAPIは、APIモードで作成したRailsアプリケーション上でscaffold
を実行することで作成しました。
今回はEvent
というモデルのREST APIを作成しました。
$ rails g scaffold event title:string
sacaffold
で作成されたエンドポイント一覧は以下の通りです。
- GET /events
- POST /events
- GET /events/{id}
- PATCH /events/{id}
- DELETE /events/{id}
OpenAPI Specificationは以下の通りです。
api.yaml
openapi: 3.0.2
info:
title: サンプルAPI
version: 1.0.0
servers:
- url: http://localhost:3000
tags:
- name: イベント
paths:
/events:
get:
tags:
- イベント
description: イベント一覧取得
responses:
200:
description: 成功
content:
application/json:
schema:
type: array
description: イベントの配列
items:
$ref: "#/components/schemas/Event"
post:
tags:
- イベント
description: イベント登録
requestBody:
content:
application/json:
schema:
type: object
properties:
title:
type: string
example: サンプルイベント
responses:
201:
description: 作成
content:
application/json:
schema:
type: object
$ref: "#/components/schemas/Event"
/events/{event_id}:
get:
tags:
- イベント
description: イベント詳細
parameters:
- name: event_id
in: path
description: イベントID
required: true
schema:
type: integer
format: int64
example: 1
responses:
200:
description: 成功
content:
application/json:
schema:
type: object
$ref: "#/components/schemas/Event"
404:
description: event not found
patch:
tags:
- イベント
description: イベント更新
parameters:
- name: event_id
in: path
description: id
required: true
schema:
type: integer
format: int64
example: 1
requestBody:
content:
application/json:
schema:
type: object
properties:
title:
type: string
example: サンプルイベント
responses:
200:
description: 成功
content:
application/json:
schema:
type: object
$ref: "#/components/schemas/Event"
delete:
tags:
- イベント
description: イベント削除
parameters:
- name: event_id
in: path
description: id
required: true
schema:
type: integer
format: int64
example: 1
responses:
204:
description: No Content
components:
schemas:
Event:
type: object
properties:
id:
description: ID
type: integer
format: int64
example: 1
title:
description: タイトル
type: string
example: サンプルイベント
created_at:
description: 作成日
type: string
format: date-time
example: 2020-04-01 10:00
updated_at:
description: 更新日
type: string
format: date-time
example: 2020-04-01 10:00
テストコードは以下の通りです。
今回はテスティングフレームワークにRSpecを利用しています。
REST APIの挙動についてはテストで確認できているが、OpenAPI Specificationを利用したスキーマ検証は行えていない状態です。
spec/requests/events_spec.rb
require 'rails_helper'
describe 'Events', type: :request do
let(:json) { JSON.parse(response.body) }
let(:request_header) { { "Content-Type" => "application/json" } }
describe 'GET /events' do
let!(:event) { create :event }
before do
get '/events'
end
it '取得できる' do
expect(response).to have_http_status 200
expect(json.length).to eq(1)
end
end
describe 'GET /events/:id' do
let!(:event) { create :event }
before do
get "/events/#{event.id}"
end
it '取得できる' do
expect(response).to have_http_status 200
end
end
describe 'POST /events' do
it '登録できること' do
expect {
post "/events",
params: { event: { title: 'サンプルイベント' } }.to_json,
headers: request_header
}.to change { Event.count }.from(0).to(1)
expect(response).to have_http_status 201
end
end
describe 'PATCH/PUT /events/:id' do
let!(:event) { create :event }
before do
patch "/events/#{event.id}",
params: { event: { title: 'サンプルイベント更新' } }.to_json,
headers: request_header
end
it '更新できること' do
expect(response).to have_http_status 200
end
end
describe 'DELEET /events/:id' do
let!(:event) { create :event }
it '削除できること' do
expect {
delete "/events/#{event.id}"
}.to change { Event.count }.from(1).to(0)
expect(response).to have_http_status 204
expect(response.body).to be_empty
end
end
end
なお、REST APIのRSpecを実行する環境の構築方法については今回省略しますので、詳細についてはRSpecでAPIのレスポンスボディのプロパティを検証する方法を参照してください。
committeeの導入方法
今回はcommittee-railsを利用してcommitteeを導入します。
Gemfile
group :test do
gem 'committee-rails'
end
$ bundle install
spec/rails_helper.rb
にcommitteeの設定を追記すればOKです。
今回はnginx/html/swagger-ui/api.yaml
に作成したOpenAPI Specificationを読み込むようにしています。
spec/rails_helper.rb
RSpec.configure do |config|
config.add_setting :committee_options
config.committee_options = { schema_path: Rails.root.join('nginx', 'html', 'swagger-ui', 'api.yaml').to_s }
end
committeeにはスキーマを検証するメソッドが3つ用意されています。
- assert_schema_conform
- assert_request_schema_confirm
- assert_response_schema_confirm
リクエストの検証をする場合はassert_request_schema_confirm
、レスポンスの検証をする場合はassert_response_schema_confirm
を利用します。
assert_schema_conform
はcommitteeのバージョンによって挙動が異なります。
今まではレスポンスのみを検証するメソッドとして提供されていましたが、これからはリクエストとレスポンスを両方検証するようになる予定です。(2020年11月現在)
assert_schema_conform
でリクエストとレスポンスの検証を両方行うようにする(つまり今までの挙動ではなくする)ためにはold_assert_behavior: false
を追加します。
spec/rails_helper.rb
RSpec.configure do |config|
config.add_setting :committee_options
config.committee_options = { schema_path: Rails.root.join('nginx', 'html', 'swagger-ui', 'api.yaml').to_s, old_assert_behavior: false }
end
もし、old_assert_behavior: false
を設定せずにassert_schema_conform
を利用すると以下のようなwarningメッセージがコンソールに表示されます。
[DEPRECATION] now assert_schema_conform check response schema only. but we will change check request and response in future major version. so if you want to conform response only, please use assert_response_schema_confirm, or you can suppress this message and keep old behavior by setting old_assert_behavior=true.
設定完了後、RSpecのファイル内でinclude Committee::Rails::Test::Methods
を宣言すればOKです。
もしくは、以下のように設定ファイルでinclude
の宣言をすることで、各RSpecのファイルでinclude Committee::Rails::Test::Methods
を記述しなくて済むようになります。
spec/rails_helper.rb
RSpec.configure do |config|
config.include Committee::Rails::Test::Methods
config.add_setting :committee_options
config.committee_options = { schema_path: Rails.root.join('nginx', 'html', 'swagger-ui', 'api.yaml').to_s, old_assert_behavior: false }
end
committeeの使い方
スキーマの検証を行たいテストに対してassert_schema_conform
を追加すればOKです。
今回はold_assert_behavior: false
の設定をしているので、assert_schema_conform
でリクエストもレスポンスも検証されます。
committeeによるスキーマの検証を追加したRSpecは以下の通りです。
冒頭で紹介した差分はassert_schema_conform
があるかないかだけです。
spec/requests/events_spec.rb
require 'rails_helper'
describe 'Events', type: :request do
let(:json) { JSON.parse(response.body) }
let(:request_header) { { "Content-Type" => "application/json" } }
describe 'GET /events' do
let!(:event) { create :event }
before do
get '/events'
end
it '取得できる' do
expect(response).to have_http_status 200
expect(json.length).to eq(1)
assert_schema_conform
end
end
describe 'GET /events/:id' do
let!(:event) { create :event }
before do
get "/events/#{event.id}"
end
it '取得できる' do
expect(response).to have_http_status 200
assert_schema_conform
end
end
describe 'POST /events' do
it '登録できること' do
expect {
post "/events",
params: { event: { title: 'サンプルイベント' } }.to_json,
headers: request_header
}.to change { Event.count }.from(0).to(1)
expect(response).to have_http_status 201
assert_schema_conform
end
end
describe 'PATCH/PUT /events/:id' do
let!(:event) { create :event }
before do
patch "/events/#{event.id}",
params: { event: { title: 'サンプルイベント更新' } }.to_json,
headers: request_header
end
it '更新できること' do
expect(response).to have_http_status 200
assert_schema_conform
end
end
describe 'DELEET /events/:id' do
let!(:event) { create :event }
it '削除できること' do
expect {
delete "/events/#{event.id}"
}.to change { Event.count }.from(1).to(0)
expect(response).to have_http_status 204
expect(response.body).to be_empty
assert_schema_conform
end
end
end
committeeで検出できる差分の例
OpenAPI Specificationで定義されたAPIの仕様と、実際のAPIの挙動に差分がなければテストはパスします。
以下ではcommitteeで検出できる差分の例について紹介します。
プロパティの型が違う場合
OpenAPI Specificationで定義しているレスポンスボディのidの型をわざとintegerからstringに変更してみます。
api.yaml
id:
description: ID
- type: integer
- format: int64
+ type: string
example: 1
実際のAPIではidはintegerで返されるので、テストを実行すると以下のようなエラーになります。
Committee::InvalidResponse:
#/components/schemas/Event/properties/id expected string, but received Integer: 194
プロパティの値がnullの場合
たとえば、Eventのtitleがnullで返ってきた場合は以下のようなエラーになります。
Committee::InvalidResponse:
#/components/schemas/Event/properties/title does not allow null values
nullを許可する場合はOpenAPI Specificationでnullable: true
をセットします。
api.yaml
title:
description: タイトル
type: string
example: サンプルイベント
+ nullable: true
committeeでより厳密にAPIを定義するための方法
OpenAPI Specificationのオプションを活用することで、APIの挙動をより厳密にチェックできます。
未定義のプロパティを検知する方法
additionalProperties: false
を追加することでスキーマで定義されたプロパティしか持てなくなります。
api.yaml
schemas:
Event:
type: object
properties:
id:
description: ID
type: integer
format: int64
example: 1
title:
description: タイトル
type: string
example: サンプルイベント
created_at:
description: 作成日
type: string
format: date-time
example: 2020-04-01 10:00
updated_at:
description: 更新日
type: string
format: date-time
example: 2020-04-01 10:00
+ additionalProperties: false
これにより、実際のAPIで利用・取得されるプロパティとOpenAPI Specificationで定義したプロパティが一致しているか厳密にチェックできます。
たとえば、プロパティ名がtitle
なのをOpenAPI Specificationでttile
としたり未定義だったりした場合、以下のようなエラーになります。
Committee::InvalidResponse:
#/components/schemas/Event does not define properties: title
必須プロパティを設定する方法
required
を利用すること必須プロパティを定義できます。
OpenAPI Specificationでrequired
として扱われているプロパティが実際のAPIに含まれていない場合、どういったエラーになるのか試してみます。
以下ではわざとdetail
という実際には未定義のプロパティを追記し、必須プロパティとしています。
api.yaml
schemas:
Event:
type: object
properties:
id:
description: ID
type: integer
format: int64
example: 1
(..略..)
+ detail:
+ description: 詳細
+ type: string
+ example: サンプルイベントの詳細情報
+ required:
+ - detail
実際のAPIにはdetail
プロパティは含まれないため、以下のようなエラーが発生します。
Committee::InvalidResponse:
#/components/schemas/Event missing required parameters: detail
まとめ
- committeeを利用することでOpenAPI Specificationと実際のAPIの挙動の差分を検証できる
- スキーマの検証はassert_schema_conformで行う
- Railsの場合はcommitt-railsを利用するとcommitteeの導入がラク
Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!