たまに落ちるテストをいい感じにリトライするCircleCI Workflowsの設定
TL;DR
rspec-retryは同じプロセスの中で再実行(問題: まじで落ちてるテストも再実行される、プロセスで保持してる状態が壊れると何度実行してももとには戻らない)
bundle exec rspec
Rerunは別プロセスで再度実行(2連ガチャ)
bundle exec rspec bundle exec rspec
この記事で紹介してるのは落ちたものに限って別プロセスで再度実行
bundle exec rspec --failure-exit-code=0 bundle exec rspec --only-failures
本題
夏といえばそうめん。流しそうめんって掴みそびれると落ちますよね。
こんかい紹介する.circleci/config.yml
はこちら! たまに落ちるRSpecのexampleを受け止めて、落ちたexampleだけリトライするCircleCIの設定です。
サンプルプロジェクトはGitHubにあります。
なおこの例ではカバレッジとかrspec_junit_formatter
の結果のマージ等してないので、そこは各自やっていって良い感じの設定をブログで書いて教えてください❗
# spec/spec_helper.rb RSpec.configure do config.example_status_persistence_file_path = 'spec/examples.txt' end
version: 2.1 executors: ruby: docker: - image: circleci/ruby jobs: test: executor: ruby steps: - checkout - run: gem install rspec - run: rspec --failure-exit-code=0 - persist_to_workspace: root: . paths: - '*' test_again: executor: ruby steps: - attach_workspace: at: ~/project - run: gem install rspec - run: rspec --only-failures workflows: test: jobs: - test - test_again: requires: [test]
2019/07/12追記: testを並列で流してるとき用
test jobは並列で実行してtest_againでは落ちたテストをまとめて流す
- diff: https://github.com/hanachin/rspec-soumen/commit/92faccfaa7c60124c54bec646dd2bfe3991804cc
- サンプルブランチ: https://github.com/hanachin/rspec-soumen/tree/parallel
- 実行例: https://circleci.com/gh/hanachin/workflows/rspec-soumen/tree/parallel
# spec/spec_helper.rb RSpec.configure do config.example_status_persistence_file_path = "spec/examples-#{ENV['CIRCLE_NODE_INDEX']}.txt" end
# .circleci/config.yml version: 2.1 executors: ruby: docker: - image: circleci/ruby jobs: test: executor: ruby parallelism: 2 steps: - checkout - run: gem install rspec - run: | TEST_FILES=$(circleci tests glob spec/**/*_spec.rb | circleci tests split) rspec --failure-exit-code=0 -- $TEST_FILES - persist_to_workspace: root: . paths: - '*' test_again: executor: ruby steps: - attach_workspace: at: ~/project - run: gem install rspec - run: | head -2 spec/examples-0.txt > spec/examples.txt tail -qn +3 $(ls spec/examples-*.txt) >> spec/examples.txt mv spec/examples.txt spec/examples-0.txt - run: rspec --only-failures workflows: test: jobs: - test - test_again: requires: [test]
2019/07/12追記2: 同じjobの中で再実行
テストを流すためのセットアップに時間がかかるような場合は同じjobの中で再実行するだけでもよさそう
# spec/spec_helper.rb RSpec.configure do config.example_status_persistence_file_path = "spec/examples.txt" end
# .circleci/config.yml version: 2.1 executors: ruby: docker: - image: circleci/ruby jobs: test: executor: ruby steps: - checkout - run: gem install rspec - run: | TEST_FILES=$(circleci tests glob spec/**/*_spec.rb | circleci tests split) echo $TEST_FILES > TEST_FILES rspec --failure-exit-code=0 -- $TEST_FILES - run: rspec --only-failures $(cat TEST_FILES) workflows: test: jobs: - test
たまにおちるテスト問題
Railsあるある稀によく落ちるテストでお悩みの皆さん、CircleCIでテスト実行していますか?
たまに落ちるテストとの向き合い方でこんな感じの問題がありました。
- たまに落ちるテストは無視できない
- たまに落ちるテストなのかどうかわからない
- たまに落ちる原因を探すのが難しい
- たまに落ちるテストを直すのが難しい
- たまに落ちるテストのために余計な時間がかかる
- マジで落ちてるテストを再実行
- マジで落ちてるテストのタイムアウトが伸びる
- たまに落ちるテストガチャ、沼、時間溶ける...。
今回はここをよくする方法を紹介します。
たまに落ちるテストのために余計な時間がかかる
以下、既知の手法と問題点についていくつか紹介します(いい感じにリトライするやりかたまで読み飛ばしてもかまいません)。
テストはたまに落ちます
テストは人間が書いています。なのでテストは実質人間です。テストはたまに落ちます、にんげんだもの。
テストがたまに落ちると何がよくないのか?
CIを通さないとリリースできない場合にリリースの妨げになってしまいます。 価値が届くまでに時間がかかってしまう。
これは私の話ですが、再度テスト通るまでの間に休憩入って無を過ごしてしまいがち。モチベが下がる。
たまに落ちるテストを人間が無視してもよいのでは?
テストがオオカミ少年みたいな状態だと「なーにどうせたまに落ちるテストだろ」でエイヤッとマージすること、あるものとします。 マジでヤバイときにコードの悲鳴を無視して、エイヤでマージすると、プロダクションが死んだり死ななかったりします。
基本的には無視はよくないです。
既知の手法: コードを直す
たまにおちるテスト、実はプロダクションコードがたまにバグっていることがあります。 プロダクションコードを直して落ちなくなった! やったね!
例: SQLでORDER BY
を指定しわすれているため、並び順を確認するテストがたまに落ちる
既知の手法: テストを直す
テストコードに問題がある場合。たまに落ちる原因調査にかけるコストに対してわりに合わなくて諦めがち
原因がわからない、外部サービスのせいで落ちてると自力では直せない、などの問題がある。
例: HTMLに外部のCDNのURL埋め込んでたらそのURLが404になっちゃった、たまに503を返す
既知の手法: タイムアウトを伸ばす
例えばCapybaraなどのテストは、たまたま時間がかかってたまに落ちがち。 ならタイムアウト伸ばせばよくね?
全体の実行時間が伸びたり、リアルガチで落ちてるテストが落ちるまでの時間が伸びたり、waitオプション渡す場合どこに時間がかかるか意識してテストコードも直するのが大変。
例: default time outを伸ばしたり重いところでwaitオプションを指定する
既知の手法: rspec-retryを使う
よく知られた方法としてrspec-retryを使う方法がある。
これはたまにじゃなくてリアルガチで落ちてるテストが多いときに無駄にリトライしてテスト時間が伸びるみたいな問題がある。 あと同じプロセスの中で再実行をかけるので、teardownがうまく行ってないと、retryする意味がなくこける。
既知の手法: CircleCIもっかいRerun
たまに落ちるテストガチャ。
全部テストを再実行すると時間がかかる、たまに落ちるテストAが通ったと思ったらたまに落ちるテストB、C、D...がひょっこり出てくるなどの問題がある
今回紹介するいい感じにリトライするやりかた
RSpecのjobを2つに分けて実行します。
1つめのjob
テストを流します。 たまにテストが落ちます。 落ちたテストの情報を2つめのjobにわたします。
2つめのjob
落ちたテストの情報を受けとります。 落ちたテストだけを再実行します。
落ちたテストがない場合、成功して終了します。
また落ちてしまった場合、2つめのjobだけを再実行します。
どこがいい感じか
rspec-retryやテストのjob全体をrerunするのと違い、落ちたテストだけを別プロセスで再実行できる。 手動でRerunする必要がなく、jobをつなげるだけで自動リトライ回数が増やせる。
実例
リポジトリはこちらです。
たまに落ちるテスト
たまに落ちるテストがあります。このテストは夏日じゃない気温の場合に冷やしそうめんがうまくなくなってテストが落ちます。
# spec/soumen_spec.rb using Module.new { refine(Integer) do def 冷やしそうめんがうまい? self >= 25 end end } RSpec.describe '冷やしそうめん' do it { is_expected.to be } context '気候変動' do let(:temperature) { rand(10..40) } subject { temperature.冷やしそうめんがうまい? } it { is_expected.to be } end end
CircleCIで以下のような設定をして、jobを2段構えにします。
version: 2.1 executors: ruby: docker: - image: circleci/ruby jobs: test: executor: ruby steps: - checkout - run: gem install rspec - run: rspec --failure-exit-code=0 - persist_to_workspace: root: . paths: - '*' test_again: executor: ruby steps: - attach_workspace: at: ~/project - run: gem install rspec - run: rspec --only-failures workflows: test: jobs: - test - test_again: requires: [test]
1つめのjobと2つめのjobをつなげる
test_again
jobのrequires
にtest
を指定し、test
jobがおわってからtest_again
jobが実行されるようにしています。
workflows: test: jobs: - test - test_again: requires: [test]
参考: requires
1つめのtest
jobでテストが失敗しても、2つめのtest_again
jobを実行する
test_again
のrequires
に指定したjobが失敗した場合、test_again
jobは実行されません。
これを防ぐため、テストが失敗しても必ず成功したように見せかけるようRSpec実行時に--failure-exit-code
オプションをわたします。
jobs: test: # 略 steps: # 略 - run: rspec --failure-exit-code=0
参考: `--failure-exit-code` option (exit status) - Command line - RSpec Core - RSpec - Relish
失敗したテストの情報を2つめのjobに渡す
RSpecの設定でexample_status_persistence_file_path
を指定すると、テストの実行結果を保存できます。
# spec/spec_helper.rb RSpec.configure do |config| config.example_status_persistence_file_path = "spec/examples.txt" end
spec/examples.txt
をCircleCIのpersist_to_workspace
を使ってtest_again
jobに渡します。
ここではcheckoutしてきたソースコードやspec/examples.txt
などworking_directory直下のファイルすべてをworkspaceに保存しています。
jobs: test: # 略 steps: # 略 - persist_to_workspace: root: . paths: - '*'
参考: Only Failures - Command line - RSpec Core - RSpec - Relish、 persist_to_workspace
2つめのjobで失敗したテストの情報を受け取る
attach_workspace
を使って2つめの jobのworking_directory(デフォルトは~/project
)に、さきほどpersist_to_workspace
で保存した1つめのjobのworking_directory以下のファイルを戻します。これでソースコード一式とspec/examples.txt
を受け取ることができました。
jobs: # 略 test_again: # 略 steps: - attach_workspace: at: ~/project
参考: attach_workspace
落ちたテストだけを実行する
RSpecに--only-failures
オプションを渡して実行するとexample_status_persistence_file_path
で指定したファイルから情報を読み取り失敗したテストだけを再実行できます。やったぜ!
job: # 略 test_again: # 略 steps: # 略 - run: rspec --only-failures
参考: Only Failures - Command line - RSpec Core - RSpec - Relish
実際の流れ
テストが落ちました。2つ目のjobで落ちているようです...。2度テストが落ちたということですね。
https://circleci.com/workflow-run/2cced3b3-59c4-4e3e-9f53-33172388b74b
1つめのtest
jobで落ちていたテストが
https://circleci.com/gh/hanachin/rspec-soumen/6
2つめのtest_again
jobでも落ちたようです。
画面をみて分かるように、1つめのtest
jobでは2つテストが実行されていましたが、2つめのtest_again
jobではtest
jobで落ちたテスト1つしか実行されていませんね。
https://circleci.com/gh/hanachin/rspec-soumen/7
ワークフローの「Rerun」をクリックして「Rerun from failed」をクリックすると落ちたjobからやり直すことが出来ます。
やりなおしたWorkflowsがこちら。やったぜテスト通った!
test jobのリンクをクリックすると分かるのですが、さきほどのtest jobと同じjobの結果に飛びます。
再実行されたのは落ちていたtest_again
jobだけです。
https://circleci.com/workflow-run/09fb370d-bbdd-4648-acba-0bebe5a7bddf
2019/07/12追記: 並列実行する場合
保存されるファイル名がかぶらないようexample_status_persistence_file_path
のファイル名に環境変数のCIRCLE_NODE_INDEX
を含めます。
example_status_persistence_file_path
の内容は単純なテキストファイルです。
example_id | status | run_time | ---------------------------------- | ------ | --------------- | ./spec/soumen_brand_spec.rb[1:1] | passed | 0.00022 seconds | ./spec/soumen_brand_spec.rb[1:2:1] | failed | 0.00022 seconds | ./spec/soumen_spec.rb[1:1] | passed | 0.00262 seconds | ./spec/soumen_spec.rb[1:2:1] | passed | 0.00289 seconds |
各コンテナーでのexample_status_persistence_file_path
の内容をまとめたい場合、ヘッダー2行を書き出したあと単純に各ファイルの3行目以降を追記していくといいです。
# ヘッダーを出力 head -2 spec/examples-0.txt > spec/examples.txt # 3行目以降をまとめて出力 tail -qn +3 $(ls spec/examples-*.txt) >> spec/examples.txt # ファイル名を1コンテナーの場合のexample_status_persistence_file_pathに合わせる mv spec/examples.txt spec/examples-0.txt
まとめ
CircleCIのWorkflowsを使ってテストのjobを2つに分けた。
- 全てのテストを実行する
- 落ちたテストを再実行する
これにより、CircleCIたまに落ちるテストRerunガチャで2のガチャだけを引けるようになり、あたりを引きやすくなりました。
また、テストコードに変更を加えずに落ちたテストだけを再実行できるようになりました。 teardown漏れてて再実行が通らないやばめのテストがあっても安心して再実行できるし、落ちたテストを自動で1回再実行してくれます。 たまに落ちるテストが稀にではなくほぼ毎回落ちる現場の場合でも2のjobを重ねればリトライの実行回数を増やせて安心♡
たまに落ちるテストのために余計な時間をかけるのは生産的ではないので、なんかいい感じのやり過ごし方を見つけて折り合いをつけていきましょう......。
もっといい方法あったらおしえてください。
最近たべたそうめんは冷やし担々麺風そうめん
魚肉ソーセージと人参のソーメンチャンプルー