第90回 Ruby関西 勉強会で発表したスライド資料です! https://rubykansai.doorkeeper.jp/events/150547
実録レガシー Rails アプリ改善ジャーニー2023.3.25 第90回 Ruby関西 勉強会 / 舘林 秀和 @shutooike
View Slide
自己紹介舘林秀和 / Shuto Tatebayashi所属株式会社インゲージ各種アカウント@shutooike途中で名字が変わってややこしい趣味ひとことコミュニティで発表するのが初めてなので緊張してます
目次ジョイン当時の状況課題改善ジャーニー自動テスト編バージョンアップ編リファクタリング編得られたものこれからまとめ
ジョイン当時の状況2014年から開発している Rails アプリフロントエンドはSPAバックエンドはAPI+Jobサービスはたくさんのユーザーに使われているRuby のバージョン2.3RSpec が導入されているが、カバレッジは20%以下テストがないことがほとんど=レガシーコードQAはステージング環境でのドックフーディングと手動テスト頼みCIはあった(werkerを使用)週に1回本番リリース
課題テストが継続的に書かれてない簡単な不具合修正、機能改善、負債返却で他が壊れることがある以前と変わることは手動テストで担保するが以前と変わっていないことを担保できていないステージング環境によるドッグフーディングには限界が変更に対する心理的安全性が低い言語・ライブラリのバージョンアップが継続的に行われていないEOL切れバージョンアップ時の防衛線がステージング環境にしかいない同じようなロジックがあちこちに点在
改善ジャーニー次のスライドから取り組んできたことおおよそ時系列で紹介していくこの発表で説明しきれない詳細は今後ブログに書いていくつもりTwitter で @shutooike をフォローして、読んで、拡散して、感想ください(欲張り)
自動テスト編
実行順序のランダム化ファイル名順での実行になっており順番に依存したテストがちらほらspec ファイルを追加すると突然落ちるようになったりconfig.order = :randomテスト数がそこまで多くなかったのでエイヤで :randomにして、ローカルやCIで落ちたら都度修正Kernel.srand config.seedとすると Array#sample も seed で再現するようになる
テストデータが残らないようにCIで特定のテストが失敗することが稀によくある他のテストで作ったデータが原因example ごとにデータを消すconfig.use_transactional_fixtures = truebefore(:all)で作りっぱなしのデータbefore(:each)に変更after(:all)で削除
自動テストを書いていく(1)日々の開発で自分が関わったコードからテストを書いていくなぜか書きにくいという実感が...原因突き詰めたところ→「そうか!土台が出来てないんだ!」プロジェクト発足
実際の最初の issue
スキップしていたモデルのコールバックを解放User.skip_callback(:create, :after, :foo)思考停止でのほぼ全てのコールバックがスキップされていたプロダクトコードではデータ作成時にコールバックで行っている処理を、テストデータ作成時は自分で明示的に処理しないといけない状況テスト環境の挙動は出来る限り本番環境に近づけるべき一部残したコールバックもある時間がかかりすぎるもの外部サービスを呼び出すものモック化するという手もある
FactoryBot のフル活用お馴染みのテストデータ生成ライブラリシナリオに沿ったテストデータをサクッと作れないとテストを書くハードルが一気に上がる導入はされているが、上手く運用はされていない状況FactoryBot の定義方針を検討・決定詳しくはブログに書きます方針に沿って FactoryBot を再定義・既存のテストを修正最初は頑張って全モデルでやろうとしたがその必要はなかったよく出てくるスタメンモデルだけでOK
自動テスト戦略を立てるここまでテストを書いた感覚でテスティングピラミッドを目指すのは厳しいと判断単体テストを継続的に書くのは開発者の力量に依存するところが大きい良くない単体テストはむしろ開発生産性を下げるプライベートメソッドのテストを書く期待値生成にコードと同じロジックを用いる... などまずは統合テストたくさん書いていくことにする目指すはテスティングトロフィー主な Rails の責務である API の request spec を書いていくとりあえずある程度までカバレッジを上げたい
誰でも書ける request spec helper を用意誰でも書けるように薄さにこだわる正常系のみ最低限の条件ログインユーザーパラメーター最低限の検証APIの権限チェックHTTPステータスコードチェックAPIを作ったら絶対書こうねという意識付け→「お約束」というネーミング
利用側のイメージdescribe 'POST /api/messages' docontext 'お約束' dolet(:login_user) { create(:normal_user) }let(:minimum_required_params) { { text: 'Hi' } }it_behaves_like(:can_be_accessed_with, 'normal')it_behaves_like(:return_http_status_as, :created)endendどのように実装したかはブログに書きます
全APIに正常系テスト(お約束)を書いていく全体カバレッジが60%ほどにAPIのカバレッジは90%近くに少なくとも最低限の正常系の動作が変わっていないことが担保できるようになったので安心感が全然違うコスパ最強
request spec でAPIの振る舞いを検証するクリティカルユーザージャーニー(CUJ)を定義ユーザーが 1 つの目的を達成するために行うサービスとの一連のインタラクションCUJをもとにサービスの主要な操作に関するAPIに対して正常系・異常系の振る舞いを検証振る舞いとは?データのCRUDジョブのキック外部サービスの呼び出し... など主要なAPIの動作の担保され安心感がさらに上がった
最低限のE2Eテストを書くSPAといいつつ複数画面があるメイン画面編集画面外部連携用画面... など最低限の検証画面が正常に開けることのみ確認ステージング環境でのドックフーディングでは普段開かない画面がひっそり死んでることに気づけるように
バージョンアップ編
Ruby 2.3 → 2.7.6警告の対応依存 gem のバージョンアップ開発環境でバージョンアップ自動テスト・手動テスト他の開発者の開発環境に展開ステージング環境バージョンアップ手動テスト本番環境バージョンアップ
Rails 5 → 6警告の対応依存 gem のバージョンアップ開発環境でバージョンアップrails app:updatenew_framework_defaults自動テスト・手動テストステージング環境バージョンアップ手動テスト本番環境バージョンアップ
リファクタリング編
複数モデルで使える共通の処理を concern にRails がデフォルトで用意している仕組み上手く使えたら綺麗に抽象化できる上手く使えたら...例created_by, updated_by を入れる処理
単体テストが書きやすくなるような実装に自動テスト戦略で統合テストに注力したもう1つの理由として単体テストが書きにくい実装になっていたドメインロジックがコントローラー層にFat Controllerより単体テストが書きやすいようにドメインロジックをモデルに寄せていく必要があるFat Model自動テストがある程度書けたので振る舞いが変わらないことを担保しながらリファクタリングができる
[再掲] 課題テストが継続的に書かれてない簡単な不具合修正、機能改善、負債返却で他が壊れることがある言語・ライブラリのバージョンアップが継続的に行われていないEOL切れバージョンアップ時の防衛線がステージング環境にしかいない同じようなロジックがあちこちに点在
得られたものテストが書きやすい環境APIを作成したらお約束テストを追加する文化ある程度のカバレッジコードの変更やライブラリのバージョンアップなどに対する心理的安全性他に影響があるコード変更がCIで落ちるライブラリのバージョンアップの非互換に気付けたことも
[再掲] 課題テストが継続的に書かれてない簡単な不具合修正、機能改善、負債返却で他が壊れることがある言語・ライブラリのバージョンアップが継続的に行われていないEOL切れバージョンアップ時の防衛線がステージング環境にしかいない→ 継続してやっていく同じようなロジックがあちこちに点在→ 継続してやっていく
これからとりあえず「EOLを切らない」を目標に Ruby, Rails を上げていくdependabot をフル活用してライブラリアップデートを習慣化テスティングピラミッドを目指す単体テストが書きやすくなるよう引き続きリファクタリングステージング環境で行っている手動テストの負荷低減現在の最低限のE2Eから少し検証範囲を広げる
まとめ自動テストは書かないと上手くならない勘所を掴むまでクソテストコードを書いて書いて書きまくれ幸いテストコードは書き直しやすいレガシーコード改善に銀の弾丸はないカバレッジも可読性、メンテナビリティも突然上がることはない理想に向けてロードマップを引き地道にやっていくしかないレガシーコードを愛するユーザーの課題を解決し、売上を立てるレガシーコードは偉大なりたくてレガシーコードになった訳じゃないその時々の最適解だったはず恨みではなくリスペクトを持って接する
おまけ1カバレッジは正しく測ろう以下の2つを行ったらカバレッジがグッと上がった# config/envioroments/test.rbconfig.eager_load = truespring を使わずに RSpec を実行原因ははっきりわかっていないがおそらくコードの分母が正しく測れるようになったから知ってる人いたら教えてください
おまけ2カバレッジを追い求めすぎないカバレッジが高いからバグが出ない訳じゃない効果的なテストをすることが一番重要カバレッジは目標にせず、指標にするべき