committee-railsで読み込むOpen APIのスキーマをディレクトリ毎に切り替えるアレ
こちらは Rails Advent Calendar 2020
の12月21日の記事になります。ていうか、Qiita Advent Calendarの記事をZennで書いていいのかな。いいよねきっと。
何書こうかなと考えてたけど、結果的に小ネタみたいな記事になってしまったんだ。すまない。まあ落ち着いて座ってくれないか。
さて、今回はRspecでOpen APIのスキーマに基づいたテストを実行する committee-rails で、ちょっと詰まったところを書いていこうと思います。
committee-rails
念のため、committee-railsについて改めて説明を少ししておきます。
簡単に言うと、Swaggerなどで記載したAPIドキュメントのSchemaと、実装したAPIのレスポンスが一致するかをチェックするassert_response_schema_confirm
メソッドを提供するgemです。
これによってリクエストを検証するRspec内で:
it 'UserオブジェクトのJSONが返却されていること' do
json = JSON.parse(request.body)
expect(json['user']['id']).to eq(1)
expect(json['user']['name']).to eq('Fukumoto')
︙ # JSONのキーが延々つづく(つらい)
end
↑みたいにJSONの検証をチマチマ書かなくてよくなります:
it 'Schemaに記載されたJSONが返却されていること' do
assert_response_schema_confirm
end
やりたいこと
Rspecでテストを実行する際に、committee-rails
が読み込むSchemaファイルをspecファイルが存在するディレクトリによって読み分けたいです。
例としては、Railsが複数アプリケーションのバックエンドを担当している場合や、同じアプリケーションのモバイル/デスクトップによってリクエストするAPIのスキーマが異なる場合などは、テストを実行する際にこのようなケースが考えられます。
今回は、以下のようなディレクトリ構造のController/RequestSpecを例として話を進めていきます。
├── app
│ └── controllers
│ └── api
│ └── v1
│ ├── mobile
│ │ └── spec.yml
│ └── spec.yml
└── spec
└── requests
└── api
└── v1
├── mobile
│ └── user_spec.rb
└── user_spec.rb
言ってはいけないこと
スキーマを分割したくなるほど複雑なAPIを同じRailsアプリケーションに乗せるな。責務が複雑になるからアプリケーションを分割しろ。
(※画像はマズそうだったら消す)
Tl;Dr
-
@committee_options
の設定をmodule化して、specファイルの@curr_directory
で読み込むmoduleを識別すればおk
動作環境
- Ruby: 2.6.6
- Rails: 5.2.3
- committee-rails: 0.5.1
コード
想定するフォルダ構成(再掲)
├── app
│ └── controllers
│ └── api
│ └── v1
│ ├── mobile
│ │ └── spec.yml
│ └── spec.yml
└── spec
└── requests
└── api
└── v1
├── mobile
│ └── user_spec.rb
└── user_spec.rb
committee-railsの設定
- 以下の記述をRspecのどこぞかのファイルに追加してください
- 私の場合は
spec/support/initializers/committee-rails.rb
というファイルを作りました
- 私の場合は
# frozen_string_literal: true
# 各APIのRequestSpecのパスを指定
DESKTOP_SPEC_DIR = '/spec/requests/api/v1/'
MOBILE_SPEC_DIR = '/spec/requests/api/v1/mobile/'
RSpec.configure do |config|
# Rspecを実行する前に1回だけスキーマファイルの読み込みを実行する
config.before(:each, type: :request) do |example|
# 各RequestSpecのディレクトリがどのAPIに該当するのかを識別する
path = example.metadata[:example_group][:file_path]
directory = if path.include?(DESKTOP_SPEC_DIR)
DESKTOP_SPEC_DIR
elsif path.include?(MOBILE_SPEC_DIR)
MOBILE_SPEC_DIR
end
# Rspecのフォルダによって、読み込むschemaを設定したmoduleを切り替える
curr_directory = config.instance_variable_get(:@curr_directory)
if curr_directory.nil? || directory != curr_directory
case directory
when DESKTOP_SPEC_DIR
config.include CommitteeDefault
when MOBILE_SPEC_DIR
config.include CommitteeMobile
end
config.instance_variable_set(:@curr_directory, directory)
end
end
end
module CommitteeDefault
# APIのSchema定義
include Committee::Rails::Test::Methods
def committee_options
@committee_options ||= { schema_path: Rails.root.join('app/controllers/api/v1/spec.yml').to_s,
prefix: '/api/v1',
parse_response_by_content_type: true }
end
end
module CommitteeMobile
# モバイル APIのSchema定義
include Committee::Rails::Test::Methods
def committee_options
@committee_options ||= { schema_path: Rails.root.join('app/controllers/api/v1/mobile/spec.yml').to_s,
prefix: '/api/mobile/v1',
parse_response_by_content_type: true }
end
end
テストコード
- 以下のように、各APIのRequestSpecで
assert_response_schema_confirm
を含むテストが(実装ができている前提で)通過すればOKです
# 例: spec/requests/api/v1/user_spec.rb
it 'UserオブジェクトのJSONが返却されること' do
assert_response_schema_confirm
end
少し解説
(他にうまいやり方をご存知の方はぜひ教えて下さい)
@committee_options
-
committee-rails
の設定ですが、基本的にはこの@committee_options
インスタンス変数にAPIスキーマのファイルとフォルダパスを格納してあげればそれでOKです- なので、今回は「この
@committee_options
をどうやっていい感じに読み替えるか」という話になります
- なので、今回は「この
-
@committee_options
の設定をしたら、あとはCommittee::Rails::Test::Methods
をincludeすれば、APIのスキーマを読み込んでのassert_response_schema_confirm
が使用可能になります
describe 'request spec' do
include Committee::Rails::Test::Methods
def committee_options
@committee_options ||= { schema_path: Rails.root.join('schema', 'schema.json').to_s }
end
describe 'GET /' do
it 'conform json schema' do
get '/'
assert_response_schema_confirm
end
end
end
(※上記のサンプルは、gemのREADMEより拝借)
prefix, parse_response_by_content_type
- スキーマを読み込む際に、
committee
側のオプションも指定できるみたいです-
prefix
はcommittee
のMiddlewareを(必要な場合に)有効にするパスの一部を指定することができるっぽい -
parse_response_by_content_type
はヘッダのContent-Type
が'application/json' のときだけレスポンスをJSONにパースしてくれる
-
- 他にもcommitteeのREADME見ると色々設定がありそうなので、細かく指定してみても良いかも
module CommitteeDefault
# APIのSchema定義
include Committee::Rails::Test::Methods
def committee_options
@committee_options ||= { schema_path: Rails.root.join('app/controllers/api/v1/spec.yml').to_s,
prefix: '/api/v1',
parse_response_by_content_type: true }
end
end
config.instance_variable_get(:@curr_directory)
- 見たまんまですが、Rspecの実行時のconfigに
@curr_directory
を設置し、そこに各APIのRequestSpecのパス(最初に定数で指定)を代入して比較することで、読み込むmoduleを変更しています
# Rspecのフォルダによって、読み込むschemaを設定したmoduleを切り替える
curr_directory = config.instance_variable_get(:@curr_directory)
if curr_directory.nil? || directory != curr_directory
case directory
when DESKTOP_SPEC_DIR
config.include CommitteeDefault
when MOBILE_SPEC_DIR
config.include CommitteeMobile
end
config.instance_variable_set(:@curr_directory, directory)
end
おまけ
Remote Reference
-
committee
の4.0.0からOpen APIのRemote Referemce
がサポートされたので、複数のスキーマをひとつのschemaファイルに統合してcommitteeを走らせることもできる(みたい)
/api/mobile/v1:
$ref: mobile.yml#/paths/~1users
Discussion