hacomono TECH BLOG

フィットネスクラブ・スクールなど施設・店舗のための会員管理・予約・決済システム「hacomono」 開発チームの技術ブログ

hacomono Rails プロダクトの改善の取り組みの話

hacomono インテグレーションチームに所属している西脇です。社内では htz (ひつじ) と呼ばれており、インテグレーション領域の開発とチームマネージメントを行っています。

先月まではエンタープライズ開発チームとして活動していましたが、エンタープライズのお客様のご要望に多い主に API の公開や、システム連携の仕組みを開発する事が多い事から、今月よりインテグレーションチームとチーム名称を変更し活動をしています。

普段はインテグレーションチームでの開発とチームマネージメントも行いつつも、Rails や RSpec 周りの改善についても行っています。今回は現在 hacomono で抱えている問題と、その問題についての取組について紹介したいと思います。

はじめに

hacomono のバックエンドアプリケーションは Ruby on Rails で実装されており、テストは RSpec で記述されています。Rails のアプリケーションは API もしくはバッチ処理になっており、フロントエンドは持っておらず、テストは

  • 単体テスト
  • リクエストテスト (内部結合テスト)

の両軸で行われており、 E2E テスト (RSpec で言うところのシステムテスト) はフロントエンドアプリ側で行っています。

全体的なテストとしてはかなり網羅的に記述されており、テストのカバレジッジとしては実に『94%』を超えており、この規模のアプリケーションとしては非常に高い数値となっており、開発者としてもお客様としても安心感が高くなっているかと思います。

課題

前述の通りカバレッジを高く保つ事はできていますが、アプリケーションの拡大と共に様々な課題が見えてきました。

テストの記述方法に統一感やルールが無く、メンテナンス性が悪い

[問題1] プロダクトも開発に携わるメンバーも増えたことの裏返しとして、それまで細かなルールやスタイルガイドが存在しなかったことも影響し、RSpecの記述方法がメンバーごとにばらばらで、テストデータの作成方法自体も統一感がない RSpec が増えています。

[問題2] また、多くのテストをリクエストテストに頼り、API のエンドポイントレベルでのホワイトボックステストで記述されており、依存する各階層の細かな仕様の違いを全てリクエストテストとして記述をしているため context ブロックのネストが深く仕様把握にも仕様変更時にも大きなコストがかかっています。

context '所属店舗、前回予約店舗、お気に入り店舗がある場合' do
  context '前回予約店舗は所属店舗と同じ店舗' do
    context '所属店舗、前回予約店舗、お気に入り店舗(すべて同じ店舗)がメンバーサイト非表示店舗の場合' do
      it '所属店舗、前回予約店舗が表示されない' do

[問題3] テストデータの作成方法が、FactoryBot の利用、モデルから作成、Usecsse 層を呼び出して作成など、さまざまです。また、 shared_context を利用 (悪用) した example 側で直接把握できない大量の let が生成されるような書き方が常用されており、仕様の把握が困難になり、テストのパフォーマンスも悪化させています。

shared_context :init_xxxxx_members do
  let!(:plan1_entity) { ... }
  let!(:plan2_entity) { ... }
  ...
  let!(:plan40_entity) { ... }

  let!(:member1_entity) { ... }
  let!(:member2_entity) { ... }
  ....
  let!(:member40_entity) { ... }
end

[問題4] 不安定なテスト (Flaky test) が頻繁に発生 (潜在) しており、Pull Request の CI においてテストが通らない事が多発しています。

全体の Example のうちリクエストテストが『82%』以上を占めている

[問題5] 全体の Example 数としてはヒミツですが、そのうち 82% がリクエストテストで構成されています。ですので、単体テストとしてはわずか 18% 程度しか記述されていません。そのため、クラス単位での問題の発見性も悪く、分岐やメソッドがあらゆる条件下において網羅されていない可能性があります。
また逆に、リクエストテストで重複した仕様をテストしている場合もあり、実装レイヤーの深い場所への仕様の追加の際には影響範囲の把握が難しく、リクエストテストでの条件 (context) の追加が難しい (漏れる) 場合が多発しています。 (問題2 にもつながっている)

実際にリクエストテストの除いたテストカバレッジを計測すると『65%』程度にまで下がってしまっていました。Rubyのカバレッジ計測では、クラスやメソッドの定義部はそのクラスやメソッドがテストで呼び出されなくても実行時に評価されるため、何もテストを書いていなくても最低でも40%程度はあります。そのため、より単体テストの記述割合が少ないことがわかるかと思います。

RSpec のみに費やす CI が並列実行を行っても『40分』近くかかってしまっている

[問題6] 問題5 の Example 数が相当多いということと、問題3 のテストデータが無駄に作成されているということも有り、1 回の CI における RSpec の実行時間が約 5 時間、6 並列化することにより約 40 分もの GitHub Actions 時間を費やしてしまっています。
プロダクトの成長とともに並列数を増やしていけば良い話ではあるが、問題を抱えた状態で札束で殴るだけの状態ではエンジニアとしてはあまりにも不幸な状態だと思っています。

解決策

RSpec コーディングガイドの作成

プロダクト自体の規模も大きく関わる開発者の人数も多くなってきたため、細かな規約は Rubocop や CI で機械的にチェックできるようにしているため (問題2 の対応)、基本的な RSpec の記述方針に対して足並みを揃えることを目的として作成しました ( 問題1 への対応)。
また、大きなパフォーマンス Issue が起きないように、これまで繰り返されてきた良くない記述方法についての禁止事項も hacomono 独自として記述しました。 (問題3 への対応)

不安定なテスト (Flaky test) を見て見ぬふりをしない

ボーイスカウトルールで自身の PR で落ちたテストは出来るだけ修正し、できる限り見て見ぬふりをしないようにしています。
自身で解決できない場合も、Slack で詳しそうな人や担当者へ共有をすることで必ず解決へ向けて前進させ、他の人が同じ問題で時間を無駄にしないようにしています。 (問題4 への対応)

単体テストとリクエストテストのカバレッジ計測を分ける

リクエストテストによる全体的なカバレッジが上がっていることの幻を防ぐためと、単体テストによるテストカバレッジの向上のため、 hacomono ではカバレッジの計測を単体テストとリクエストテストに分け別々に計測するようにしました。 (問題5 への対応)

  • 単体テストのカバレッジ・・・コントローラーを除いたコードカバレッジ
  • リクエストテストのカバレッジ・・・コントローラーのみのコードカバレッジ

これにより、単体テストを記述することの重要性を伝えるとともに、これまで単体テストが足りていなかった部分が原因で起きていたインシデントを減らすことを期待しています。

parallel_test の導入と、既存 RSpec のリファクタリング

GitHub Actions による Matrix による RSpec の水平分割は行っていたが、parallel_test の利用はできていませんでした。
CircleCI 時代に利用していた形跡があったため、GitHub Actions でも利用するような修正を行いました。 (問題6 への対応)

また、単体テストの追加と同時にリクエストテストを削減したり、パフォーマンスの悪い RSpec のリファクタリングを行いこまめにパフォーマンスの改善を行いました。
それにより Matrix による並列数を増やすこと無く、改善の開始当初より半分近く CI 待ち時間を短縮する事ができています。 (問題2, 6 への対応)

まとめ

いかがでしたでしょうか?現在、 hacomono で抱えている課題とそれに対する解決策についてご紹介させて頂きました。
少しずつではありますが課題の改善に向けて進めることができており、引き続き楽しみながら改善も進めていこうと思っています。


株式会社hacomonoでは一緒に働く仲間を募集しています。
採用情報や採用ウィッシュリストもぜひご覧ください!