RSpecテストの並列化で実行速度を飛躍的に向上させた話

はじめに

こんにちは、プロダクト開発エンジニアの梶山です。

私たちのチームでは、プロジェクトの成長に伴いテスト数が増加し、CIの実行時間が徐々に長くなっていました。開発サイクルの最適化のため、テスト実行の高速化に取り組みました。

今回は、RSpecテストの並列化による実行時間短縮の取り組みについて、手法から実装方法、そして得られた効果までを紹介します。

1. 課題

テスト実行の重要性と課題

私たちのチームでは、コード品質と安全性を維持するためにテストを重視しており、プルリクエストのマージ前にCIパイプラインでのテスト通過を必須としています。新機能の開発やバグ修正を行う際には、必ずテストが追加されそれをCIで実行し、全てのテストがパスしてから本番環境へのデプロイが可能になります。

しかし、プロジェクトの成長に伴い、以下の問題が顕在化してきました

  • テスト実行時間の増加: テスト数の増加に伴い、CIでのテスト実行時間が30分を超えていました
  • 開発サイクルの遅延: PRのレビュー→テスト実行→マージ→デプロイというサイクルが遅くなり、機能のリリースまでの時間が長くなっていました
  • 頻繁な再実行: 一部のテストが不安定で、テスト失敗時に再実行が必要になることがあり、さらに時間を消費していました

これらの問題を解決するため、テスト実行の高速化が必要不可欠でした。

2. parallel_rspecによる並列化

テスト実行を高速化するための第一歩として、RSpecテストを並列実行できるGemであるparallel_rspecの導入をしました。

並列化による効果測定

ローカル環境ではCPUのコアを利用してコア数分並列でテストを実行することができ、単純にコア数分の一の時間に短縮ができました。これはかなりの改善ですが、GitHub Actionsのdefault runnerは2コアであり、1/2程度の改善にしかなりませんでした。

3. GitHub Actionsのmatrixを活用した並列数のスケール

CIでの実行をさらに高速化するため、GitHub Actionsの matrix 機能を活用し、テストを複数のジョブに分割して実行する方法を導入しました。

GitHub Actionsのmatrix機能の説明

GitHub Actionsの matrix 機能を使うと、異なる環境や条件で同じジョブを並列に実行できます。これを活用して、テストスイート全体を複数の部分に分割し、それぞれを別々のジョブで実行することで、全体の実行時間を短縮できます。

公式ドキュメント: https://docs.github.com/ja/actions/writing-workflows/choosing-what-your-workflow-does/running-variations-of-jobs-in-a-workflow

並列数の決定方法と最適化

最適な並列数を決定するために、様々な並列数でテストを実行し、実行時間とコスト(GitHub Actionsの実行時間)のバランスを考慮し、4matrix 2並列、すなわちテスト実行は8分割での設定としました。これ以上並列数を増やしても、ジョブのセットアップ時間などのオーバーヘッドが大きくなり、全体の実行時間はあまり短縮されませんでした。

並列数増加による効果測定

計8並列での実行により、テスト全体の実行時間が約20分から約7分に短縮されました。これは約65%の時間短縮であり、開発サイクルを大幅に効率化することができました。

4. Actions Cacheを利用したDBダンプキャッシュ

テスト実行のたびにデータベースの準備に時間がかかっていることに気づき、これを最適化するためにGitHub Actionsのキャッシュ機能を活用しました。

テスト実行時のDB準備のオーバーヘッド

ジョブの実行ログを分析すると、各テストジョブでデータベースのセットアップに約1分30秒かかっていることがわかりました。この影響で並列数を上げてもCI完了時間が早くなりません。

DBダンプのキャッシュ戦略

この問題を解決するため、以下のようなアプローチを取りました:

  1. データベースのスキーマとシードデータをダンプしてキャッシュに保存
  2. キャッシュが有効な場合、ダンプからデータベースを復元
  3. キャッシュが無効な場合のみ、通常のデータベース準備を実行

キャッシュ追加による効果測定

DBダンプのキャッシュを導入することで、テスト全体の実行時間がさらに短縮され、約7分から約5分になりました。特に、PRの更新のたびにテストを再実行する場合に、大きな効果が得られました。

5. 実装結果と今後の展望

実装前後の実行時間の比較

各施策による実行時間の短縮効果をまとめると以下のようになります

実行時間 短縮率(累計)
導入前 30min -
parallel_rspec導入 20min 33%
GitHub Actions matrix 7min 77%
DBダンプキャッシュ 5min 83%

結果として、当初の約30分超から約5分へと、約83%の時間短縮を達成することができました。

各施策がどの程度効果があったかの分析

  • parallel_rspec導入: 並列化の導入により、実行時間の短縮を恒常的に実現可能に
  • GitHub Actions matrix: 複数のマシンでの並列実行により、並列数のスケールを確保
  • DBダンプキャッシュ: 各ジョブのセットアップ時間を短縮し、全体の実行時間の最適化

チーム開発への影響

テスト実行時間の短縮により、以下のような効果が得られました:

  • 開発サイクルの高速化: PRのレビュー→テスト実行→マージ→デプロイのサイクルが短縮
  • フィードバックの迅速化: テストの結果をより早く得られるようになり、問題の早期発見が可能に
  • 開発者のストレス軽減: テスト実行待ちの時間が短縮され、開発者の満足度が向上
  • コードレビューの効率化: レビュー後の修正→テスト→再レビューのサイクルが短縮

課題と今後の改善点

実装後も以下のような課題が残っています:

  • 遅いジョブに引っ張られる: 並列実行の場合、最も時間のかかるジョブが全体の実行時間を決定します。一部のテストが極端に時間がかかる場合、それがボトルネックになります。
  • 遅いテストが固まるとそれがボトルネックとなる: テストの分割方法によっては、遅いテストが特定のジョブに集中してしまうことがあります。

これらの課題に対して、以下のような改善策を検討しています:

  • テストの実行時間に基づいた分割: テストの実行時間を記録し、それに基づいてより均等に分割する方法の導入
  • 遅いテストの最適化: 極端に時間のかかるテストを特定し、それらを最適化または分割する

まとめ

RSpecテストの並列化により、テスト実行時間を大幅に短縮することができました。この改善により、開発サイクルが高速化され、チームの生産性が向上しました。また、今後テストが増え合計実行時間が増えたとしても並列数を増やすアプローチを取ることで簡単に対処が可能です。テスト実行の高速化は単に時間を節約するだけでなく、開発者体験を向上させ、より迅速かつ頻繁にフィードバックを得られる環境を整えることにつながります。今後も継続的な改善を通じて、品質を維持しながら開発速度を高めていくための基盤として活用していきたいと考えています。

herp.careers