Committee × Railsでのスキーマ駆動なAPI開発|Offers Tech Blog
概要
Offers を運営している株式会社 overflow の磯崎です。
テックブログを書き始めて早数ヶ月、ただひたすら OpenAPI について書き記しているのが私です。そろそろ違うことを書きたいです。
さて今回は、定義した API と実装した API の response が一致しているかを rspec でテストしていく模様を記述します。
弊社でのスキーマ駆動 開発フロー
- API 定義
- フロントエンド開発
- 定義した API から通信処理を自動生成し、それを叩く(API 定義をベースに通信)
- バックエンド開発
- 定義した API 通りに開発
- 定義通りの response を返す実装ができているかをテストで保証(API 定義との乖離防止)← 今回の記事はこちらについて書いています
何も特別なことはしておらず、まず API を定義し、その後にその定義との乖離が起きないようにフロント、バックエンドと並行して開発を進めていきます。
ここでバックエンドが保証すべきものとしては、「定義した API とのずれがないこと」なのでそれをテストでカバーしていこうといった取り組みです。
使用するgem
これを入れることで、assert_response_schema_confirm
という定義した API とテスト対象の API response が一致しているかを確認してくれるメソッドが使用できるようになります。
初期設定
spec/rails_helper.rb
内の rspec の設定に committee の設定を追加します。
config.add_setting :committee_options
config.committee_options = {
schema_path: Rails.root.join('schema', 'api.yaml').to_s,
prefix: '/v1',
}
ほぼ readme 通りに設定しています。
細かく設定したい場合は、大本である committee を参照してください。
バージョニングをしている場合など、request path に prefix をつけている場合は、prefix を設定します。
使用例
実際に動かしてみるのがわかりやすいので、テストを実行してみます。
今回はコメントを取得する API を例とします。
'/comments/{comment_id}':
parameters:
- schema:
type: string
name: comment_id
in: path
required: true
get:
summary: コメント取得
tags: []
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
title:
type: string
author:
type: string
operationId: get-comments
このようにテストを書くだけで、とっても簡単に response と定義した schema が一致しているかを確認できます。
describe 'GET /api/v1/comments/:id' do
let(:comment_id) { 1 }
it 'ok' do
get "/api/v1/comments/#{comment_id}"
assert_response_schema_confirm(200)
end
end
テスト成功パターン
まずは成功パターンです。
以下のように定義した response 通りに json を返してあげる API を作成しました。
def get_comment
render json: {
title: 'title',
author: 'name'
}, status: :ok
end
実行結果
テストが通りました。
GET /api/v1/comments/:id
ok
1 example, 0 failures
テスト失敗パターン
返り値の型が違う
ここでは失敗の例として、srting として定義した title を int で返してみます。
def get_comment
render json: {
title: 1,
author: 'name'
}, status: :ok
end
実行結果
きちんと、string で定義されているのに int が返ってきてますよと忠告してくれますね。
GET /api/v1/comments/:id ok
Failure/Error: assert_response_schema_confirm(200)
Committee::InvalidResponse:
#/paths/~1comments~1{comment_id}/get/responses/200/content/application~1json/schema/properties/title expected string, but received Integer: 1
responseに必ず含まれるプロパティには、requiredをつける
先程の例をそのまま使用します。
次は、「定義した値が response に含まれていない」場合どうなるのかをみていきます。
パッと期待する挙動としては、テストが失敗し、〇〇が含まれていませんと忠告を受けることかと思います。
タイトルを response から除外して、テストを実行してみます。
def get_comment
render json: {
author: 'name'
}, status: :ok
end
普通にテストが通ってしまいました。
GET /api/v1/comments/:id
ok
1 example, 0 failures
このように、「response に必ず含まれる key」の場合は、API 定義の際に required
をつける必要があります。
先程の api.yaml に required
を追加します。
'/comments/{comment_id}':
parameters:
- schema:
type: string
name: comment_id
in: path
required: true
get:
summary: コメント取得
tags: []
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
title:
type: string
author:
type: string
required: #こちらを追加
- title
- author
operationId: get-comments
そして、再度をテストを実行するとこのように失敗します。
Committee::InvalidResponse:
#/paths/~1comments~1{comment_id}/get/responses/200/content/application~1json/schema missing required parameters: title
required のタイトルがないですよと忠告してくれるようになります。
なので、response に必ず含まれるものとして定義しているものには required をつけます。
additionalProperties: falseは絶対につける
次は、「定義していない値が response に含まれている」場合どうなるのかをみていきます。
これもパッと期待する挙動としては、テストが失敗し、余計な〇〇が含まれていますと忠告を受けることですね。
location
という余計な値を突っ込んでみました。
def get_comment
render json: {
title: 'title',
author: 'name',
location: 'tokyo'
}, status: :ok
end
このまま実行すると、普通にテストが通ります。
GET /api/v1/comments/:id
ok
1 example, 0 failures
これは地味に危険です。本来出すべきではない値のチェックが行われずに、テストが通ってしまいます。
極端な例をあげると、ユーザーを返す API で password も紛れ込んでいた場合などは、それをテストで落とすことができません。
これを解決するためには、additionalProperties: false
を API 定義ファイルに追加します。
'/comments/{comment_id}':
parameters:
- schema:
type: string
name: comment_id
in: path
required: true
get:
summary: コメント取得
tags: []
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
title:
type: string
author:
type: string
required:
- title
- author
additionalProperties: false #これを追加
operationId: get-comments
再度同条件でテストを実行します。
Committee::InvalidResponse:
#/paths/~1comments~1{comment_id}/get/responses/200/content/application~1json/schema does not define properties: location
定義されていない location というのが紛れ込んでいますよ!という文章とともにテストが落ちるようになりました。
このように response 内に定義していないものが紛れ込んでいないかもチェックできるので、この additionalProperties: false
は原則つけて良いものだと考えています。個人的にはデフォルトで設定しておいてほしいレベルです。
nullable: trueの取り扱い
OpenAPI3 から出現した nullable: true
は、付与することで値に null が入っていてもテストが通るようになります。
def get_comment
render json: {
title: nil,
author: 'name',
}, status: :ok
end
nullable: true を付与しないと、テスト失敗します。
Committee::InvalidResponse:
#/paths/~1comments~1{comment_id}/get/responses/200/content/application~1json/schema/properties/title does not allow null values
nullable: true を title に追加して、再度実行してみるとテストが通ります。
また、key 自体が存在しないパターンでもテストが通ります。
'/comments/{comment_id}':
parameters:
- schema:
type: string
name: comment_id
in: path
required: true
get:
summary: コメント取得
tags: []
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
title:
type: string
nullable: true #これを追加
author:
type: string
required:
- title
- author
additionalProperties: false
operationId: get-comments
OpenAPI での null の扱いについては、様々な意見が渦巻いておりますが個人的にはこのように考えております。
- nullable: true は原則使わない。required をつけて空文字など初期値を入れておく
- それでも null を返す必要がある場合は、response の key 自体に含めない
とにかく、response を null or 文字列など複雑なものにしたくないからです。
response のパターンが増えれば、当然ながらフロントエンド開発に負担をかけることになります。
API の実装者としてはシンプルで扱いやすい設計を心がけるという初心は忘れずにいたいです。
そして、弊社は json builder として、active_model_serializers を使用しています。
シリアライザーは、has_one
や belongs_to
で関連するものがない時は、key 自体が response に含まれません。
なので、そこでもばらつきがでないように、上記のようなことを心がけて API の response を設計しております。
まとめ
スキーマ駆動開発における、API の response の整合性の保証を rspec で行う方法について書きました。
テスト自体は gem をいれることですごくシンプルに記述できるので、快適にテストを追加していけております。
課題としてあがるのは、どちらかというと OpenAPI の運用面であり、上述した注意点を守らないと意図しない API が爆誕する可能性もあります。
ただし、しっかりと API さえ定義できていればあとはシンプルな記述で response のチェックが行えるので、可読性の高いコードで結果を保証できます。
なので、スキーマ駆動開発という名の通り、この開発フローにおいてはスキーマの定義が最も重要です。
できるだけ、人間が気をつけなくても良いような仕組みを作り、安心安全に開発を進めていくために引き続き色々と試していきます。
これにて、私の OpenAPI 連載シリーズ終了です。
関連記事を載せてありますので是非合わせてご一読ください。
ちなみにこれらスキーマ駆動開発の経験、課題感を踏まえ現在部分的な GraphQL 移行が進んでます。
関連記事
副業転職の Offers 開発チームがお送りするテックブログです。【エンジニア積極採用中】カジュアル面談、副業からのトライアル etc 承っております💪 jobs.overflow.co.jp
Discussion