かまたま日記3

プログラミングメイン、たまに日常

CircleCI上のRSpecのテスト時間をparallel_split_testを使って短縮する

CircleCIを使ってテストを実行する場合、テストはcircleci tests splitコマンドで分割した上で複数コンテナを使うことでテストの実行時間を短縮することができます(参考リンク)。ただ、コンテナ数を増やすのも限界がありますし*1、少し前までtiming-based splittingがCircleCI Workflowでは有効にならないという問題もあり、コンテナごとのテストケース数や実行時間に偏りがあって、結局一番遅いコンテナに引きづられてスループットが思ったより上がらないという問題がありました。*2

そこで、コンテナ上でさらにテスト(RSpec)を並列実行することで、コンテナごとの実行時間を短縮し、それによって全体のスループットを上げるというのを試みてみました。

Gem探し

RSpecを並列実行するGemは何個かあって、rrrspec, parallel_tests, rspec-parallelなどがありますが、一長一短があり、どれもしっくり来ない感じでした。悩みポイントとしては、こちらのqiitaの記事で書かれていることがかなり近いです。

qiita.com

こちらの記事のyuku_tさんがが作られたparallel-rspecも試してみたのですが、まだProduction Readyでないようで通常のRSpecの挙動と違うところがあり、自分のケースでは使えませんでした。

それで色々他を探した結果 parallel_split_test というGemに行き着きました。こちらは最小限のハックでRSpecを拡張していて、なおかつファイルベースではなくspecベースでテストを分散できるという、まさに自分の求めるものでした。*3

使ってみる on CircleCI

使い方は至ってシンプルで、 rspec コマンドを parallel_split_test に変えるだけです、rspecで使っているオプションは全部使える(はず)です。 CircleCI的な実行の仕方としてはこんな感じになると思います。デフォルトのmediumコンテナは2CPUなので並列実行数(PARALLEL_SPLIT_TEST_PROCESSES)は2を指定しています。

export PARALLEL_SPLIT_TEST_PROCESSES=2
bundle exec parallel_split_test --profile 10 \
  --format RspecJunitFormatter \
  --out /tmp/test-results/rspec.xml \
  --no-merge \
  --format progress \
  $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)

--no-merge オプション

このオプションはparallel_split_test独自のオプションです。これを付けない場合、 アウトプットで指定したファイル*4に各プロセスの実行結果がすべてマージされます。 RspecJunitFormatter の場合各プロセスの実行結果それぞれが個別のxmlファイルになって、マージされるとxmlのフォーマットではなくなってしまうので、--no-mergeオプションをつけることで別々のファイルに書き出します。ファイル名は、もともと指定したものにプロセス番号が追加されます*5。CircleCIは複数の結果ファイルをよしなにマージして集計してくれるので、この方法で全部のテスト結果を集計できます。

パッチ当てた

また、使ってる中で何個かはまったポイントはPRだして修正したりして、自分の使っているケースでは今はちゃんと動いています。 https://github.com/grosser/parallel_split_test/pull/13 https://github.com/grosser/parallel_split_test/pull/14 https://github.com/grosser/parallel_split_test/pull/15

よかったら、使ってみて下さい!

*1:お金が無限にあればその限りではない

*2:time-based効かない問題は2018/01ごろに解決しています、参考リンク

*3:ちょっとgemの名前がジェネラル過ぎるんじゃないかとか、このGem自体のテストが難しいというのもあったりしますが...w

*4:ここでは /tmp/test-results/rspec.xml

*5:この例の場合 /tmp/test-results/rspec.1.xml のようになる