committee-railsで読み込むOpen APIのスキーマをディレクトリ毎に切り替えるアレ

2020/12/21に公開

こちらは 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です。

https://github.com/willnet/committee-rails

これによってリクエストを検証する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側のオプションも指定できるみたいです
    • prefixcommitteeの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

https://github.com/interagent/committee/pull/266

https://swagger.io/docs/specification/using-ref/

Discussion