ある案件で、GitLab CI/CDでRSpecによるテストを自動化しています。
その際に以下の問題がありました。
- テスト完了まで30分ほどかかる
- たまに落ちるテストがある
CIでは、テスト成功後に開発環境へ自動デプロイするようになっているため、たまに落ちるテストによりテスト全体を再実行することになり、デプロイ完了まで多くの時間がかかってしまっていました。
改善方針
たまに落ちるテストをいい感じにリトライするCircleCI Workflowsの設定 を参考にさせていただき、失敗したテストのみ実行するretry-testというステージをRSpecを実行するtestステージの後に追加し、only-failuresオプションで失敗したテストのみ再実行できる構成にしました。
ステージを分けることでテストの再実行に失敗しても、retry-testステージのジョブのみをさらに再実行することも可能になります。
もちろん、将来的には
- テスト速度の改善
- たまに落ちるテストの原因調査
など根本原因を解決する必要はありますが、一時的にでも再実行の手間を改善したくこの方針を取りました。
spec_helper.rbでconfig.example_status_persistence_file_path = 'spec/examples.txt'
を指定しテスト結果をspec/examples.txtに出力していることを前提として話を進めます。
また、GitLab 17.2.1で動作確認しています。
関連箇所の.gitlab-ci.ymlは以下になります。
# .gitlab-ci.yml
rspec:
stage: test
script:
- bundle exec rspec
- echo "RSPEC_RESULT=success" >> result.env
allow_failure: true
artifacts:
paths:
- spec/examples.txt
when: on_failure
expire_in: 1 hrs
reports:
dotenv: result.env
retry-rspec:
stage: retry-test
script:
- |-
if [ "$RSPEC_RESULT" = "success" ]; then
exit 0
fi
if [ -e "spec/examples.txt" ]; then
grep failed spec/examples.txt
bundle exec rspec --only-failures
else
echo "examples.txtが存在しません。ステージ: testのrspecを再実行してください。"
exit 1
fi
スクリプトの中身をこのあと説明していきます。
testステージで失敗しても次のステージに進めるようにする
GitLab CI/CDではジョブが失敗した場合にパイプラインの実行を継続するかどうかをallow_failureで制御します。
今回の改修ではtestステージで失敗しても、retry-testステージに進んでほしいため、allow_failure: true
を指定してtestステージでの失敗を許容しています(この設定により、retry-testステージでの処理に抜けがあると本当に失敗しているテストがあるにも関わらずCIが成功することがあるため、CIの成功後に自動で本番環境へデプロイする構成にしている場合は注意が必要かと思います)。
アーティファクトでステージ間でファイルを共有する
testステージのRSpec実行で出力したexamples.txtを次のステージでも利用するために、アーティファクト を利用しています。
テスト再実行に使うだけのため有効期限は1時間にしています。
デフォルトでは、各refの最新コミットの成功したパイプラインのアーティファクトはexpire_in
の値に関わらず常に保持されるため、この設定を変えていなければ有効期限は短くして良いかと思います。
artifacts:
paths:
- spec/examples.txt
when: on_failure
expire_in: 1 hrs
また、テストが成功したかどうかを環境変数に保持し、次のステージで参照できるようにしています。
参考: GitLab CI/CD variables | GitLab
rspec:
stage: test
script:
- docker-compose run --rm web bundle exec rspec
- echo "RSPEC_RESULT=success" >> result.env
こうすることでretry-testステージでtestステージでテストに成功しているかを判別することができ、テストの再実行処理をスキップすることができます。
失敗したテストの再実行
retry-testステージでは以下を行なっています。
- testステージでテストが成功していたらそれ以上処理を続けずに成功にする
- examples.txtがなければエラーにする
- テスト実行前に異常終了した場合、アーティファクトの有効期限が切れた場合などexamples.txtが存在しない場合に誤って成功扱いにならないようにエラーにする
- 失敗したテストをgrepでジョブログに出力し再実行する
retry-rspec:
stage: retry-test
script:
- |-
if [ "$RSPEC_RESULT" = "success" ]; then
exit 0
fi
if [ -e "spec/examples.txt" ]; then
grep failed spec/examples.txt
bundle exec rspec --only-failures
else
echo "examples.txtが存在しません。ステージ: testのrspecを再実行してください。"
exit 1
fi
終わりに
たまに落ちるテストの修正、テスト実行時間の短縮など根本的な対策が必要だという話はありますが、失敗するテストが数件程度であれば、再実行は時間がかからないため、追加の時間をかけずにデプロイまで進めることができるようになりました。