JavaScriptを有効にしてください

committeeを使ったSchema駆動開発

 ·   ·  ☕ 8 分で読めます  ·  🐯 ブシトラ

実務で、committeeを導入したので、その時のメモを書く。

経緯と問題点

弊社のサービスAは、モノリスなRailsのアプリケーションであるが、その他社内サービス複数からAPIサーバとして使われているが、Swaggerに関して色々問題があった。

Swaggerの明確な書き方がなく、エンジニアが既存コードを模倣して書いている 一つのymlファイルがアホほど大きくなり、可読性が悪い。 実際のSwaggerと実装が微妙に乖離している部分がある

など、新しいAPIを生やすのはいいが、それを管理できずに、APIについての確認依頼が飛んできて、Aサービスのエンジニアと別部署の人間がコミュニケーションを取らざるを得なかった。

そこで、名前だけ知っていたスキーマ駆動開発をググってみた所、committeeというgemを使うと良さそう。といった流れである。

committeeの説明

沢山記事があるので詳細は割愛するが、committeeは下記。

committeeは、実際のAPIリクエストやレスポンスがスキーマ定義にそっているかをチェックすることができるgemで、
実際のAPIリクエストやレスポンスがスキーマ定義にそっているかをチェックすることができ、Rackのミドルウェアとして動作します

公式gem を色々見るとだいたい分かる

committee Rails

ただ、committeeをRailsで使えるようにするのには、committee-railsが必要。

よく使うメソッドはこれ。一応参考まで

実際よくつかうのはこれ → assert_response_schema_confirm

使い方

1
2
  gem "committee"
  gem "committee-rails"

をインストール。
rails_helper.rb に、下記を追加。(pjによって Rails.root.join の後は変える必要がある)

1
2
3
4
5
config.include Committee::Rails::Test::Methods #毎回各Specで includeするのは面倒なので
config.add_setting :committee_options
config.committee_options = { schema_path: Rails.root.join('schema', 'schema.json').to_s,
parse_response_by_content_type: false # なくてもいいけどWarningが出ます
# prefix: "/api/v1" ← とすると、yamlでapi/v1とかかなくていいので楽だが、ここはお好みで

2つのgemをいれることにより、assert_response_schema_confirm が使えるようになる。
assert_response_schema_confirm は、書かれたドキュメントとレスポンスが一致してるかテストしてくれる代物。後述する。

例 getするとuserのidとnameを返す

routing

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
module Api
  def self.extended(router) # rubocop:disable Metrics/MethodLength
    router.instance_exec do
      namespace :api do
        namespace :v1 do
           get "swagger_sandbox", to: "tests#sandbox" unless Rails.env.
           <!-- 説明のため適当 -->
           production?
        end
      end
    end
  end
end

controller

getしたら適当にuserのidとnameを返すようにする

1
2
3
4
5
class Api::V1::TestsController < Api::BaseController
  def sandbox
    render json: { user: { id: 1, name: "busitora" } }, status: :ok
  end
end

Spec

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
require "rails_helper"

RSpec.describe Api::V1::TestsController, type: :request do

  describe "GET #sandbox" do
    subject {
      get(api_v1_swagger_sandbox_path,
      headers: { Authorization: "Token token=#{ENV["API_TOKEN"]}" })
    }

    before do
      subject
    end

    context "when success" do
      let(:return_http_status) { 200 }

      it "return expected status" do
        expect(response).to have_http_status(return_http_status)
      end

      it "return expected body schema" do
        assert_response_schema_confirm
      end
    end
  end
end

assert_response_schema_confirm が呼ばれた時に、rails_helper.rb で設定した
schema_path: Rails.root.join("swagger", "openapi_sandbox.yaml").to_s のファイルを読みに行く。
今回は、 openapi_sandbox.yaml と設定した。

yaml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
openapi: 3.0.5
info:
  title: OpenAPIテスト
  version: 1.0.0
  description: OpenAPIテスト
servers:
  - url: http://localhost:3000
    description: Local server
  - url: https://staging.test.tokyo
    description: Staging server
paths:
  /api/v1/swagger_sandbox:
    get:
      summary: Get User
      description: ユーザー1件を取得
      responses:
        "200":
          description: ユーザー1件を取得
          content:
            application/json:
              schema:
                type: object
                properties:
                  user:
                    $ref: "#/components/schemas/UserModel"
components:
  schemas:
    UserModel:
      type: object
      required:
        - id
        - name
      properties:
        id:
          type: integer
          example: 1
          description: primary id
        name:
          type: string
          example: busitora
          description: 名前
        additionalProperties: false #これを追加すると、propertiesで許容してないのも弾く

などと書く。イメージついただろうか。

成功

APIの返り値とyamlの期待値があっている

上記yamlの書き方で、json: { user: { id: 1, name: “busitora” } } の期待値は、
idnameが必須であるという設定になる。
この段階でテストすると成功する

失敗

yamlにrequiredを追加するが返り値に追加しない

1
  render json: { user: { id: 1, name: "busitora" } }, status: :ok
1
2
3
4
  required:
    - id
    - name
    - age #追加する

結果は下記
Committee::InvalidResponse: #/components/schemas/UserModel missing required parameters: age

「yamlにはageがrequireになってるのに、追加されてないよ」と言ってくれる。

返り値の型が違う時

1
  render json: { user: { id: 1, name: "busitora", age : "27" } } status: :ok

integerなのにstringにした

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
  required:
    - id
    - name
    - age
  properties:
    id:
      type: integer
      example: 1
      description: primary id
    name:
      type: string
      example: busitora
      description: 名前
    age:
      type: integer
      example: 27
      description: 年齢
    additionalProperties: false

結果は下記
Committee::InvalidResponse: #/components/schemas/UserModel/properties/age expected string, but received Integer: 27

型はintergerだけど、stringでかえってきてるやんけエラー

不要な値が入っている時(additionalProperties: false を外した時)

1
  render json: { user: { id: 1, name: "busitora" , age: 27, address: "yokohama" } }, status: :ok
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
components:
  schemas:
    UserModel:
      type: object
      required:
        - id
        - name
        - age
      properties:
        id:
          type: integer
          example: 1
          description: primary id
        name:
          type: string
          example: busitora
          description: 名前
        age:
          type: integer
          example: 27
          description: 年齢
        # additionalProperties: false これは、不要なプロパティを許容するかどうか

結果は下記
Committee::InvalidResponse:#/components/schemas/UserModel does not define properties: address

このオプションはPJであわせておかないと大変なことになる。

ポイントや注意点

ControllerSpecだと、そもそも動かなかったのでRequestSpecで書く必要がある。
弊社はContorollerSpecが多すぎて、そこを置き換えるがまず死ぬほどかかった。まだ残ってるのもある

オブジェクトでモデルを返す時は、テスト対象で必須なカラムのみrequiredにするべき → DBのnull が許容されているカラムに関して、 nullable: true を死ぬほど書くことになる。
そのフィールド、nullable にしますか、requiredにしますか

responseが 、 json: "" だとテスト出来ないので、destory以外はしっかりレスポンスで操作した対象を返すべき

よく使う用語やオプション

assert_response_schema_confirm → エンドポイントのjsonのレスポンスとschemaの整合性をチェック
prefix: “/api”, → 任意指定できる
nullable: true → レスポンスにはあるが、nilのもの

導入してみて

yamlが正義になるのでSwagger関連のやり取りが減る API生やすときにレスポンスどうするか問題を統一できる specを強制的に書く習慣が生まれる(弊社はまだまだ強制はできてない)

全部は書けていないが、導入する価値はあったと思う。

今後直したいこと

jsonで返している値が、jbuilderだったり、レスポンスを200か204で返すかなど、まだルールが明確に決まっていないところがある。

ドキュメントのない、いわゆる化石コードのAPIを修正出来ずに、assert_schema_conform の型に合わせられず修正できていない部分がある

まとめ

これからAPIを開発する時は、

  1. Schemaに必要な情報を網羅する
  2. 開発
  3. RequestSpec

としたい。そうすることで、ドキュメントとコードの整合性が保たれると思う.

参考

swaggerとopenapiの違い

もともと Swagger という名前だったものが、 OpenAPIと名前を変えてバージョン3.0がリリースされました。
Swaggerと聞けば馴染みのある方も多いと思います。
基本的にSwaggerとOpenAPIを読み替えても問題はないのですが、(ドメインとか残ってるし => https://swagger.io/specification/
Swaggerは2.xまでで、OpenAPIは3.0からになるので、Swaggerのバージョン3というものは厳密には存在しません。

とのこと

資料や動画

[JA] How to use OpenAPI3 for API developer / @ota42y

Rails + RSpec + OpenAPI3 + Committeeでスキーマ駆動開発を運用するTips

一言

テスト書いてほしい。。。

GraphQLってなにそれおいしいのなので調べてみたい

共有

busitora
著者
ブシトラ
エンジニア