CIでのテスト実行時間を1/3にした
Circle CIでのRspecの実行時間が長くなっていて、並列化すると実行時間を短縮できそうだったのでやってみるとハマりながらもなんとかいい感じにできたので紹介したいと思います!
環境
Circle CI + Ruby + Rails + Rspec
結論
version: 2.1
jobs:
test:
parallelism: 2 # 並列化したい数字にする
steps:
- checkout
- run: bundle check --path=vendor/bundle || bundle install --path=vendor/bundle --jobs=4 --retry=3
- run: mkdir ~/rspec
- run:
command: |
TESTFILES=$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
bundle exec rspec --format progress --format RspecJunitFormatter -o ~/rspec/rspec.xml -- $TESTFILES
when: always
- store_test_results:
path: ~/rspec
上記の用に書けばいい感じに並列化してRspecを実行しているのですが、ハマったところなども紹介しておこうと思います
commandの解説
まず command:
の部分なんですが一度長ったらしいコマンドの結果を環境変数 TESTFILES
に代入して、その環境変数を rspec
コマンドの引数に渡していることがわかると思います
これは spec/ 配下のファイルをglob
-> 実行時間によっていい感じに分割
をやっていて公式のドキュメント に書いてありますが、実行時間以外にもファイル名やファイルサイズによって分割することもできます
ファイルサイズはともかく実行時間はどこで判定しているのか疑問に思う方もいると思いますが、これは rspec
コマンドの引数を見てみると -o ~/rspec/rspec.xml
とありここでテストの実行結果を保存していて、これによっていい感じにテストの実行時間によって振り分けを実現しています
なので一回目の実行時には実行結果が残っていないため、実行時間による振り分けが行われるのは二回目以降になります
あと --format RspecJunitFormatter
のオプションによってrspecの実行結果をフォーマットしているんですが、これは rspec junit formatter というgemが必要なのでGemfileに追記しておく必要があります
group :test do
gem "rspec"
gem "rspec_junit_formatter"
end
ハマったところ
結果だけ見るとたった数行ですが、これに行き着くまで結構ハマってしまいました
その1
テストの実行結果を保存するオプション -o ~/rspec/rspec.xml
がありましたがこの ~/rspec/
ディレクトリはもともとのリポジトリには存在しないためあらかじめ - run: mkdir ~/rspec
で作成しておく必要があるんですがこの一行の書き忘れて結構時間溶かしました…
あとはCircle CI側でテスト結果をアップロードおよび保存する設定も忘れずに書いておく必要があります
- store_test_results:
path: ~/rspec
参考: https://circleci.com/docs/ja/2.0/configuration-reference/#storetestresults
その2
もうひとつハマったのが使用しているリポジトリのディレクトリ構成が少しイレギュラーだったためうまくパスを指定できておらずハマってしまいました
(通常は hoge_app/app
となっているところ hoge_app/foo/app
と一つfooディレクトリ配下にRailsアプリがある感じです)
最初は echo $PWD
とか挟んだりしてデバッグしてたんですが一行コード変えてプッシュしてCIが走るの待つのはなかなか時間の無駄だったので、SSH接続してみると実行中のコンテナに入ってコマンド叩いてファイルは作成されているのか?とかコマンドの実行結果がどうなってるのか?とかすぐに確認できてめちゃくちゃ捗りました
ログにエラー出てたり想定通りの動作をしないときなんかはSSHで確認するのめちゃくちゃオススメです!
(テストがコケたときに再実行するときに目にしていた Rerun Job with SSH
の意味がようやくわかってスッキリした)
参考: https://dev.classmethod.jp/articles/circleci-job-contener-ssh-connect/
最後に
並列化してテストを実行するまではすぐに実現できたんですが、それぞれの実行結果の時間を見ると数分ズレていたのでどうせならちゃんと時間揃えたいな〜と思って作業してたら思いのほかハマってしまいましたがなんとかできてよかったです!
まあまあしんどかったですが、おかげでCircle CIに関しての知識もついたし、チームのみんなにも喜んでもらえたようなのでやってよかったと思います!
似たような環境構成だとこの記事を参考にすればすぐできると思うので是非やってみてください〜
参考
追記
テスト実行の並列化の以降Flakyなテストが増えてしまってせっかくCIの実行時間が減ったのに再実行しないといけないみたいになってしまい、面倒くさいうえに開発効率落ちちゃうので対応策を追記します
落ちたテストを再実行するコマンドを追加
Flakyなテストを根本的に落ちないテストへと改善するのも一つだと思いますが、今回はより工数を掛けずに問題を解決するため落ちたテストのみを再度実行することにしました。これでたまに落ちるテストがたまたま2連続で落ちない限りCIはパスするのでこれで十分じゃないかなと思ってます
test:
steps:
- checkout
- run: bundle check --path=vendor/bundle || bundle install --path=vendor/bundle --jobs=4 --retry=3
- run: mkdir ~/rspec
- run:
command: |
TESTFILES=$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)
- bundle exec rspec --format progress --format RspecJunitFormatter -o ~/rspec/rspec.xml -- $TESTFILES
+ echo $TESTFILES > TESTFILES
+ bundle exec rspec --failure-exit-code=0 --format progress --format RspecJunitFormatter -o ~/rspec/rspec.xml -- $TESTFILES
- store_test_results:
path: ~/rspec
+ - run:
+ name: retry failed tests
+ command: |
+ bundle exec rspec --only-failures $(cat TESTFILES)
参考記事では再実行するJobを追加するコードも紹介されてましたが同じJobで再実行するのが一番手っ取り早そうなのでこれを採用しました
これで解決!!
参考:
Discussion
記事を書いていただきありがとうございます。
並列実行の時だけ落ちるテストがあって、それを修正することにとらわれていましたが、失敗したテストだけ再実行するアプローチもあることを気づかせてもらえました。