Open APIとCommitteeを利用したRSpecによるAPIテスト

はじめに

イタンジ株式会社で物件管理くんの開発をしている三島です。

物件管理くんでは、RailsによるAPIサーバを採用しており、テストにはRSpecを利用しています。 本記事ではOpen APIとCommitteeを使用し、RSpecでAPIテストを行う方法について記載します。

Committee

Committeeは、OpenAPIで定義したスキーマに基づいて、アプリケーションのリクエストとレスポンスの検証を行うミドルウェアを提供してくれます。

Committee::RailsはCommitteeのラッパーライブラリでrailsへの導入を容易にし、OpenAPIの仕様の保証、開発の整合性と品質を向上させるのに役立ちます。

github.com

導入方法

1.Gemfileにcommittee-railsを追加し、$ bundle installを実行します。

gem 'committee-rails'

2.RSpecで利用できるようにrails-helper.rbに以下の記述を追加します。

schema_pathには利用したいファイルのパスを記載してください。

RSpec.configure do |config|
  config.include Committee::Rails::Test::Methods

  config.add_setting :committee_options
  config.committee_options = {
    schema_path: Rails.root.join('etc/docs/openapi.yml'),
  }

上記の設定だけでRSpecのrequest specでassert_response_schema_confirmなどの検証メソッドが利用できるようになります。

https://github.com/willnet/committee-rails#normal-usage

RSpecによるAPIテスト

以下のようなスキーマが定義された投稿を扱うPostモデルを取得するエンドポイントでcommittee-railsを利用したレスポンスの検証を考えてみます。

  • OpenAPIの定義
openapi: 3.0.0
paths:
  /api/v1/posts/{id}:
    get:
      summary: 投稿の情報
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: 投稿の取得
          content:
            application/json:
              schema:
                  $ref: '#/components/schemas/Post'
components:
  schemas:
    Post:
      type: object
      properties:
        id:
          type: integer
        title:
          type: string
      required:
        - id
        - title
      additionalProperties: false
  • controller
module Api
  module V1
    class PostsController < ApplicationController
      def show
        @post = Post.find(params[:id])
        render json: @post
      end
    end
  end
end
  • request spec
RSpec.describe Api::V1::PostsController, type: :request do
  describe '#show' do
    subject(:request) { get api_v1_post_path(post) }

    let(:post) { create(:post) }

    it do
      request
      assert_response_schema_confirm(200)
    end
  end
end

こちらでテストを実行すると正常系の検証を成功させることができます。 assert_response_schema_confirmは引数にステータスコードを設定でき、レスポンスと指定したステータスコードにリクエストの結果がなっているかを検証できます。

以下のように、正常系の検証の箇所でassert_response_schema_confirm(400)とすると、本来は200になる箇所が400を想定したテストになるので、テストは失敗します。

F

Failures:

  1) Api::V1::PostsController#show
     Failure/Error: assert_response_schema_confirm(400)

     Committee::InvalidResponse:
       Expected `400` status code, but it was `200`.
     # /usr/local/bundle/gems/committee-5.0.0/lib/committee/test/methods.rb:32:in `assert_response_schema_confirm'
     # ./spec/requests/api/v1/posts_controller_spec.rb:9:in `block (3 levels) in <top (required)>'

Finished in 0.08364 seconds (files took 2.86 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/requests/api/v1/posts_controller_spec.rb:7 # Api::V1::PostsController#show

適切にレスポンスを検証するためのOpenAPI定義

committee-railsによる検証を適切に、より厳格に行うには以下の2つが重要です。

  • OpenAPI定義で必須項目はrequiredを指定する。

    • requiredを指定しないpropertiesはレスポンスに含まれているかどうか検証されず、テストをパスしてしまいます。 なので、検証したい必須な要素はrequiredを指定するようにしましょう。
  • additionalProperties: falseを設定する。

    • additionalProperties: falserequiredを組み合わせることでOpenAPIに定義されていない値がレスポンスに含まれているかを検知できるようになります。 これにより、本来レスポンスに含めるべきではない情報を誤って返すことなどを未然に防ぐことができ、より厳格にレスポンスを検証できるようになります。

先ほどのPostモデルでcontentという要素を扱うようにして、レスポンスは変わらずidとtitleのみが定義されている状態を考えます。 現在のcontrollerでは取得した@postをそのままrenderしているので、レスポンスにはid, title, contentが含まれます。

この状態でテストを実行すると以下のようになります。

  1) Api::V1::PostsController#show
     Failure/Error: assert_response_schema_confirm(200)

     Committee::InvalidResponse:
       #/components/schemas/Post does not define properties: content
     # /usr/local/bundle/gems/committee-5.0.0/lib/committee/schema_validator/open_api_3/operation_wrapper.rb:39:in `rescue in validate_response_params'
     # /usr/local/bundle/gems/committee-5.0.0/lib/committee/schema_validator/open_api_3/operation_wrapper.rb:34:in `validate_response_params'
     # /usr/local/bundle/gems/committee-5.0.0/lib/committee/schema_validator/open_api_3/response_validator.rb:20:in `call'
     # /usr/local/bundle/gems/committee-5.0.0/lib/committee/schema_validator/open_api_3.rb:41:in `response_validate'
     # /usr/local/bundle/gems/committee-5.0.0/lib/committee/test/methods.rb:40:in `assert_response_schema_confirm'
     # ./spec/requests/api/v1/posts_controller_spec.rb:9:in `block (3 levels) in <top (required)>'
     # ------------------
     # --- Caused by: ---
     # OpenAPIParser::NotExistPropertyDefinition:
     #   #/components/schemas/Post does not define properties: content
     #   /usr/local/bundle/gems/openapi_parser-1.0.0/lib/openapi_parser/schema_validator.rb:63:in `validate_data'

スキーマではcontentは定義されていませんが、実際のレスポンスにはcontentが含まれているため、スキーマと実装の不一致を検知して、意図通りにテストが失敗することを確認できました。

それでは、スキーマ定義に以下のようにcontentを追加して再度テストを実行します。

components:
  schemas:
    Post:
      type: object
      properties:
        id:
          type: integer
        title:
          type: string
        content:
          type: string
      required:
        - id
        - title
        - content
      additionalProperties: false
Randomized with seed 43980
.

Finished in 0.07156 seconds (files took 2.41 seconds to load)
1 example, 0 failures

スキーマと実装の乖離がなくなり、テストを通すことが出来ました。

導入してみて

OpenAPIとCommitteeを利用したテストの仕組みを導入したことで新たに以下の点で効果があったと思います。

  • スキーマと実装の一致を保証

    • committee-railsを使用することで、スキーマと実際のAPIレスポンスの乖離を検出することができるようになりました。 これにより、スキーマの信頼性を高めることができ、スキーマベースでの開発、コミュニケーションができるようになりました。
  • テストの強化

    • スキーマに基づいたテストを行うことで、APIの仕様変更があった際のテストの更新が容易になりました。
  • ドキュメントとしての価値

    • OpenAPIスキーマは、APIのドキュメントとしても機能し、APIの利用方法をスキーマで理解できるようになりました。

まとめ

今回はOpenAPIとCommitteeによるテストについて書かせてもらいました。 committee-railsを利用することでテストの開発体験の向上、スキーマと実装の乖離を減らす仕組みを持たせることが出来ました。 この記事がどなたかの参考になれば幸いです。ありがとうございました。