🌵

CIでのテスト実行時間を1/3にした

2022/06/06に公開
1

Circle CIでのRspecの実行時間が長くなっていて、並列化すると実行時間を短縮できそうだったのでやってみるとハマりながらもなんとかいい感じにできたので紹介したいと思います!

環境

Circle CI + Ruby + Rails + Rspec

結論

.circleci/config.yml
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に追記しておく必要があります

Gemfile
group :test do
  gem "rspec"
  gem "rspec_junit_formatter"
end

ハマったところ

結果だけ見るとたった数行ですが、これに行き着くまで結構ハマってしまいました

その1

テストの実行結果を保存するオプション -o ~/rspec/rspec.xml がありましたがこの ~/rspec/ ディレクトリはもともとのリポジトリには存在しないためあらかじめ - run: mkdir ~/rspec で作成しておく必要があるんですがこの一行の書き忘れて結構時間溶かしました…

あとはCircle CI側でテスト結果をアップロードおよび保存する設定も忘れずに書いておく必要があります

.circleci/config.yml
- 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に関しての知識もついたし、チームのみんなにも喜んでもらえたようなのでやってよかったと思います!
似たような環境構成だとこの記事を参考にすればすぐできると思うので是非やってみてください〜

参考
https://circleci.com/docs/ja/2.0/parallelism-faster-jobs/#running-split-tests
https://qiita.com/yassun-youtube/items/4f5b4e312261af9cb52f
https://qiita.com/rvillage/items/dd60c41317918b5a5872


追記

テスト実行の並列化の以降Flakyなテストが増えてしまってせっかくCIの実行時間が減ったのに再実行しないといけないみたいになってしまい、面倒くさいうえに開発効率落ちちゃうので対応策を追記します

落ちたテストを再実行するコマンドを追加

Flakyなテストを根本的に落ちないテストへと改善するのも一つだと思いますが、今回はより工数を掛けずに問題を解決するため落ちたテストのみを再度実行することにしました。これでたまに落ちるテストがたまたま2連続で落ちない限りCIはパスするのでこれで十分じゃないかなと思ってます

.circleci/config.yml
  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で再実行するのが一番手っ取り早そうなのでこれを採用しました
これで解決!!

参考:
https://hanachin.hateblo.jp/entry/2019/07/10/010613

Discussion

So-sanSo-san

記事を書いていただきありがとうございます。
並列実行の時だけ落ちるテストがあって、それを修正することにとらわれていましたが、失敗したテストだけ再実行するアプローチもあることを気づかせてもらえました。