実務で、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” } } の期待値は、
id とnameが必須であるという設定になる。
この段階でテストすると成功する
失敗
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を開発する時は、
- Schemaに必要な情報を網羅する
- 開発
- 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ってなにそれおいしいのなので調べてみたい