ローカル上でAPIテストの練習用にWebAPIを叩ける環境を作りました
はじめに
テスト自動化の学習用の練習サイトをイメージして、ローカル(Docker上)[1]でWebAPI(システム内部向けAPI)を叩ける環境を作りました!↓のリンク先リポジトリに公開しています!
この記事では、作った物に関する説明や感想などを書いていきます。
作った理由
テストコードを書いてみたいな~と何となく思っていました。そんな時にテスト自動化の学習用の練習サイトを思い出しました。このサイトは、E2Eテストの練習で時々使用していました。ふと「E2Eテストの練習はあるけど、WebAPI用の練習サイトってあるのかな?あれば良いのにな~」と思い浮かびました。
少し検索してみて、WebAPIをGETだけではなくPOSTまで気軽に叩けるような環境が見つからなかった[2]ので、テスト自動化の学習用の練習サイトの模してWebAPI版を作ってみるのは面白そうだし、ボリューム感もちょうど良さそうと思ったので作ってみました!
そして、プロを目指す人のためのRuby入門[改訂2版]やEveryday Rails - RSpecによるRailsテスト入門を読み終えたタイミングだったので、RubyとかRailsとかRSpecで作ってみました。
頑張ったこと、工夫したこと
TDD(テスト駆動開発)のように開発する
先にテストコードを書いて、動くプロダクションコードを書いて、プロダクションコードをリファクタリング、みたいな感じで進めました。今までTDDのように進めてコードを書いたことが無かったので、どんな感じになるか試してみました。
結果的に高いテストカバレッジとなりました!修正してデグレした時も、テストコードがfailedになることでデグレしてると教えてくれることは、すごく助かりました。カバレッジ9割は安心感がすごくあります。
カバレッジレポートの画像
単体テスト(Model Spec)と結合テスト(Request Spec)でエクスペクテーションの方法を変える
単体テスト(Model Spec)はit1つにつき1エクスペクテーション、結合テスト(Request Spec)はit1つにつき複数のエクスペクテーション(aggregate_failuresで囲む)として、テストコードを記述しました。これは、Everyday Rails - RSpecによるRailsテスト入門で書いていたことの実践です。
Everyday Rails - RSpecによるRailsテスト入門より引用
Model Specについて
example(it で始まる1行)一つにつき、結果を一つだけ期待している。...(中略)...こうすれば、example が失敗したときに問題が起きたバリデーションを特定できます。原因調査のためにRSpecの出力結果を調べる必要はありません。少なくともそこまで細かく調べずに済むはずです。
aggregate_failuresについて
RSpec はテスト内で失敗するエクスペクテーションに遭遇すると、そこで即座に停止して失敗を報告することです。残りのステップは実行されません。しかし、RSpec 3.3ではaggregate_failures (失敗の集約)という機能が導入され、他のエクスペクテーションも続けて実行することができます。これにより、そのエクスペクテーションが失敗した原因がさらによくわかるかもしれません。
私は統合テストでaggregate_failures をよく使います。aggregate_failures を使えば、同じコードを何度も実行して遅くなったり、複雑なセットアップを複数のテストで共有したりせずに、テストが失敗した複数のポイントを把握することができます
実践して良かった点ですが、it1つにつき1エクスペクテーションは、テストがfailedになっても、どのテストがfailedになったのかを見るだけで原因調査を深くやらなくても修正できました。
また、WebAPIのテストではリクエストの結果としてのレスポンスを確認しました。レスポンスは確認する内容が複数あることがほとんどだったので、aggregate_failuresを使用することによって、仮にどこかでfailedになっても、レスポンスの中で期待通りでない部分を全て把握できて、修正箇所の特定が早くできました。
1リクエストで1画面を表示できるようなレスポンスの設計
テスト自動化の学習用の練習サイトの各画面のURLと基本的に同じになるように、APIエンドポイントを作成しました。
初めはリソースベースのAPIエンドポイント(/usersならユーザーの情報だけ、/reservesなら予約情報だけを取得)として作成しようしていました。ただ、フロント側を考えると画面構成のために2回以上WebAPIを叩きたくないな、と思いました。[3] 具体例として、宿泊予約画面のときplanデータのプラン名以外に、userとして名前、電話番号、emailが必要な場合です。(ログイン中であれば、ユーザー情報が画面に自動設定される仕様です。)
リソースが本来違うけども、画面で必要だから別リソースもレスポンスに含めたかったので、リソースベースの設計はやめて、1画面を表示できるようなレスポンスにしました。
レスポンスのイメージ
{
"plan": {
"plan_id": 3,
"plan_name": "お得なプラン",
"room_bill": 6000,
"min_head_count": 1,
"max_head_count": 9,
"min_term": 1,
"max_term": 9
},
...(略)...
"user": {
"username": "テス太郎",
"tel": null,
"email": "tesutotaroh@example.com"
}
}
宿泊予約の仮登録と本登録のユーザーが一致することの表現
宿泊予約画面で「予約内容を確認する」ボタンを押した後、宿泊予約確認画面で「この内容で予約をする」ボタンを押せば、予約が完了となる仕様です。これを再現するために、前者を仮登録として/reserve
、後者を本登録として/reserve/{reserve_id}
で表現しました。/reserve
を叩くとreserve_id
とsession_token
を返します。その値含んで/reserve/{reserve_id}
を叩く必要があります。
この仕様にした理由について、テスト自動化の学習用の練習サイトでは、ログイン状態でなくても宿泊予約登録は出来る仕様です。どのように仮登録したユーザーと本登録しようとするユーザーを一致させようか少し悩みました。そして今回reserve_id
はシーケンシャルな連番としたので、連番は推測できる値のため、本登録を連番だけで行うことはしないだろうなと考えました。[4]そこで、tokenを払い出してそのtokenと一致していることで、仮登録したユーザーと本登録しようとするユーザーを一致できると判断して、採用しました。
頑張らなかったこと
画像のアップロード
テスト自動化学習用練習サイトでは、会員登録後、マイページ画面でアイコンを変更できます。WebAPI経由で画像のアップロードは少し頑張れば実装できるのですが、知見が無く時間が掛かりそうだったので、今回対応しませんでした。
テーブル設計でN対Nのテーブルを作らない
宿泊予約画面の右側に部屋の情報が表示されます。部屋によっては「セパレート式バス・トイレ」、「ユニット式バス・トイレ」、「独立洗面台」が表示されます。
これをテーブルで表現するには、以下のようにN対Nの関係性にする必要がありそうでした。
room_types=部屋ごとの種類情報
room_types_facilities_relations=中間テーブル
room_facilities=部屋ごとの設備情報
N対Nにした時のテーブルイメージ詳細
room_types
id | room_type_name | room_category_name | min_capacity | max_capacity | room_size |
---|---|---|---|---|---|
1 | シングル | « NULL » | 1 | 1 | 14 |
2 | ツイン | スタンダード | 1 | 2 | 18 |
3 | ツイン | プレミアム | 1 | 3 | 24 |
room_types_facilities_relations
room_id | room_facility_id |
---|---|
1 | 2 |
2 | 2 |
2 | 3 |
3 | 1 |
3 | 3 |
room_facilities
room_facility_id | room_facility_name |
---|---|
1 | セパレート式バス・トイレ |
2 | ユニット式バス・トイレ |
3 | 独立洗面台 |
ただ今回はめんどくさいので今後データが増えるものでもないため、N対Nの関係せずroom_typesテーブルに配列型のfacilitiesカラムにしました。
今回作成したテーブルのイメージ
room_types
id | room_type_name | room_category_name | min_capacity | max_capacity | room_size | facilities |
---|---|---|---|---|---|---|
1 | シングル | « NULL » | 1 | 1 | 14 | [ユニット式バス・トイレ] |
2 | ツイン | スタンダード | 1 | 2 | 18 | [ユニット式バス・トイレ,独立洗面台] |
3 | ツイン | プレミアム | 1 | 3 | 24 | [セパレート式バス・トイレ,独立洗面台] |
エラーメッセージ
エラー発生時、レスポンスのエラーメッセージが英語なので、画面上でそのまま表示できるようなエラーメッセージになっていません。
ハマったこと
Rails7 APIモードの時にDeviseでエラーが出る
ActionDispatch::Request::Session::DisabledSessionError (Your application has sessions disabled. To write to the session you must first configure a session store):
以前はwardenに渡されるセッションはHashだったが、今はセッションはActionDispatch::Sessionを使用するように変わったとのこと。Rails APIモードではActionDispatch::Sessionが無効になっているので、書き込み不可になっているのでエラーになっているらしいです。[5]
この問題は、https://github.com/heartcombo/devise/issues/5443#issuecomment-1009779292 のコードを追加したら解決しました。
Devise Token Authでエラーが出る
該当ドキュメントを見つけるまでに時間が掛かりましたが、ドキュメントを見たらすぐ解決しました。
詳細はコチラです。
Swaggerで期待するレスポンスヘッダーが表示されない
単純にCORSの問題でした...(CORSをちゃんと理解しろ)
詳細はコチラです。
RSpec実行時にseedデータが消える問題
たまに、RSpec実行時にseedデータが消えてエラーになる時がありました。
恐らく原因は、コイツです。
begin
# RSpec実行時にmigrationが発生することで、DBのレコードが全削除され、seedデータが無くなる時がある
ActiveRecord::Migration.maintain_test_schema!
rescue ActiveRecord::PendingMigrationError => e
puts e.to_s.strip
exit 1
end
対策として、RSpec実行時、毎回テーブルのデータを全削除→seedデータ投入としました。
RSpec.configure do |config|
config.before(:suite) do
# RSpec実行時にmigrationが発生することで、DBのレコードが全削除され、seedデータが無くなる時がある。その時テストが落ちてしまう。
# 以下はその対策。
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.clean_with(:truncation)
load Rails.root.join('db', 'seeds.rb')
end
end
学びになったこと
- HTTPステータスコード
今までは、エラー発生時のHTTPステータスは400とか500で実装していました。エラー発生時のロジックのほうが注力されていて、HTTPステータスは全く考慮していませんでした。今回、エラー毎のHTTPステータス(400番台)をきちんと設定してみました。(適切なHTTPステータスを設定できているのかについては、少し自信がないですが...) - ISO 5218の存在
テスト自動化の学習用の練習サイトのデータを見ていて気付きました。0,1,2,9って意味ありそうだな、と検索して性別コード存在を知りました。恐らく、この性別コードに合わせて作られてたのでは?と推測しています。 - 少しRuby、Rails、RSpecに詳しくなった
コードを書いてきたので当たり前と言えば当たり前なのですが。使えば便利なメソッドなどを知れたので、少しは詳しくなった...はずです。
終わりに
1~2ヶ月くらいで完成するかな?と思っていたのですが、設計の迷いや、エラーでハマってしまったり、他の事で忙しかったりと、思ったより時間が掛かってしまいました...
もう少し修正が必要な部分がありますが、とりあえずやり遂げれたことが良かったです。
また、RailsやRubyを約3年ぶりくらいに触って懐かしい気持ちになりつつ、作る楽しさを実感できたことと、テストコードを書きたかった欲を満たせたことも良かったです。
気が向いたときにまた何か作りたいと思います!
作る上で参考にさせていただいたリンク
-
ホスティングした場合にサイトの維持費用を掛けたくなかったので、WebAPIを叩ける環境はローカル(Docker上)にしました。 ↩︎
-
作り始めて気付いたのですが、WebAPIのサンプルサイトとして、Swagger Petstoreを見つけました。全くもってググり力が足りませんね。練習としてWebAPIを気軽に叩きたいなら、エンドポイントも多いのでこちらを使った方が良いかもしれませんね。今回は何か作る楽しみを得たかったので、作成を続けました。 ↩︎
-
今回フロント側は作成してません。フロント側を作る気持ちを妄想して考えました。 ↩︎
-
もし一般公開するプロダクトだったら...と妄想して考えました。 ↩︎
-
https://github.com/heartcombo/devise/issues/5443#issuecomment-1000921521 ↩︎
Discussion