はじめに
OPTiM Biz の開発をしております。伊藤です。
本記事は、OPTiM TECH BLOG Advent Calendar 2025 Day 8 の記事です。
OPTiM Bizはモノリシックな Ruby on Rails で開発されており、15年近い歴史を持つサービスです。その歴史の中で、単体テストのケース数は 10万5千ケース まで成長しました。
現在、OPTiM Biz は Ruby 3.4、Ruby on Rails 8.0 で動いています。これまでの度重なるバージョンアップを支えてくれたのはこの膨大な単体テストであり、プロダクトの維持に非常に重要な存在です。 しかし、多くの単体テストを回し続けるのはコストが掛かります。
- 合計実行時間:17時間35分
- すべてのマージリクエストで全テストを実行するのは非現実的
- しかし、開発中は少しでも早くテストによるフィードバックがほしい
この記事では、10万ケース超えの単体テストと向き合い、どのように効率的にテストを実行しているかをご紹介します。
(余談ですが、Ruby 4.0/Ruby on Rails 8.1の対応も現在進めています。先日Bundlerの4.0もリリースされました!試したいです)
OPTiM Bizの単体テスト運用
現在、OPTiM Bizの単体テストの状況は以下のようになっています
- RSpecで記述されている
- 合計のケース数は10万5千
- 単体テストのみで、systemテストやE2Eテストなどは含まない
- 合計実行時間は17時間35分
工夫していること
これらの単体テストを複数のチーム/エンジニアで実行し続けることはコストが大きすぎると考えています。 テストの待ち時間を減らしたり、実行計画を工夫したりしているので紹介させてください。
工夫1: 50並列実行
まずは並列実行です。GitLab CI の parallel機能 を利用して、50並列 でテストを実行しています。
実行環境は主にGitLab Runnerをk8sでオートスケールするように運用しており、自動的な縮退や安いインスタンスを選ぶなど、様々なコスト削減を行って費用を減らす努力をしています。
# .gitlab-ci.yml のイメージ rspec: stage: test retry: max: 1 interruptible: true services: - name: mysql:latest variables: GIT_DEPTH: '1' RAILS_ENV: test cache: key: files: - Gemfile.lock paths: - vendor/bundle/ artifacts: when: always paths: - report/rspec.xml reports: junit: - report/rspec.xml rules: - if: $CI_COMMIT_BRANCH == "master" parallel: 50 script: - bundle config set --local path 'vendor/bundle' - bundle config set --local with 'test' - bundle install --local - bundle exec rake db:create db:migrate - bundle exec rspec --format RspecJunitFormatter --out report/rspec.xml
これにより、すべてのテストを回す時間は合計の17時間から1時間ほどまで短縮されています。
今後の課題
50並列にしても各ノードでセットアップの時間が長いために時間の短縮効果が限定されてしまっているため、セットアップの時間を短くする活動をしていきたいと思います。 また、50並列という数字は「昔のGitLab CIの最大並列数が50だったから」という理由で最適化されていません。並列数の最適化も進めたいと考えています。
工夫2: 実行ファイルの選定
すべての開発ブランチで10万ケースのテストを実行するのは効率的ではないと考え、マージリクエスト単位では変更箇所などから関連するテストのみを実行する工夫をしています。
この仕組みを提供してくれているのがLaunchableというサービスです。(※Launchableは現在はCloudBeesに買収されたため、ホームページはCloudBeesのものになっています)
LaunchableはRubyの本体にも利用されており、テスト結果をAIで解析し様々な洞察を与えてくれます。
その中でも「Predictive Test Selection」という機能を特に利用させていただいています。この機能は詳しく説明すると複雑なのですが、簡単に言うと「先に実行すべきテストを過去のテスト実行結果を元にAIでリストアップしてくれる」ものです。
# .gitlab-ci.yml イメージ # テストを開始するジョブ start-rspec-for-mr: stage: build variables: GIT_DEPTH: '1' rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" artifacts: paths: - subset.txt - launchable-session.txt script: - ~/.local/bin/launchable record build --name "${CI_PIPELINE_ID}" - ~/.local/bin/launchable record session --build "${CI_PIPELINE_ID}" > launchable-session.txt - ~/.local/bin/launchable subset --confidence 90% --build "${CI_PIPELINE_ID}" --split rspec subset.txt # 50並列でテストを実行するジョブ rspec-for-mr: stage: test retry: max: 1 interruptible: true services: - name: mysql:8.0.31 variables: GIT_DEPTH: '1' LAUNCHABLE: enabled cache: key: files: - Gemfile.lock paths: - vendor/bundle/ artifacts: when: always paths: - report/rspec.xml reports: junit: - report/rspec.xml timeout: 10h needs: - start-rspec-for-mr rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" parallel: 50 script: - bundle config set --local path 'vendor/bundle' - bundle config set --local with 'test' - bundle install --local - bundle exec rake db:create db:migrate - > ~/.local/bin/launchable split-subset --subset-id $(cat subset.txt) --bin $CI_NODE_INDEX/$CI_NODE_TOTAL rspec | tee /builds/bizweb/optimal_biz_web_admin/target_rspec_files.txt - xargs bundle exec rspec --format RspecJunitFormatter --out /builds/bizweb/optimal_biz_web_admin/report/rspec.xml < /builds/bizweb/optimal_biz_web_admin/target_rspec_files.txt
こちらを利用して、マージリクエストでまず実行すべきテストをリストアップしてから並列実行しています。これによってGitLab CI Runnerのリソースを約77%削減することができました。
今後の課題
Launchableはテスト選択だけでなく、テスト改善のための洞察も提供してくれます。
「flakyなテスト」や「実行時間の長いテスト」、「全然失敗しないテスト」などのテスト改善に有効な情報を提供してくれるのですが、それを活かしきれていません。
flakyなテストは現在ほぼ検出されていない(嬉しい!)のですが、実行時間の長いテストや失敗しないテストはしっかり確認して、少しづつでも改善していきたいと思っています。
終わりに
15年の歴史を持つ OPTiM Biz が、どのように10万の単体テストと向き合っているかをご紹介しました。
テストは 書くだけでなく運用していく設計も重要 です。AIによるテスト選択は、開発者体験とテスト品質の両立を可能にしてくれる手段だと思います。
Ruby/Rails のバージョンアップやテスト改善に興味がある方、ぜひ一緒に働きませんか?