はじめに
こんにちは。サーバーサイドエンジニアの佐野きよ(@Kiyo_Karl2)です。
CircleCIにはRerun failed test
という機能があります。
今回はこの機能を有効化する手順について紹介していきたいと思います。
Rerun failed testとは?
Rerun failed test
とはCircleCIで提供されている標準機能のひとつで、CIを実行したときにあるテストが失敗したら、その失敗したテストケースのみ再実行することができる機能です。(これはGitHub Actionsでは提供されていない機能なので、CircleCIを利用するメリットのひとつと言えます。)
これができるようになるメリットとして、Flaky testの再実行コストを大幅に削減できるということが挙げられます。
CIを実行したときに、改修箇所とは全く関係無いテストが落ちていて、実はそれがFlaky testでテストがパスするまで何度かRerunしたという経験は無いでしょうか?
Rerun failed test
を有効化すると、失敗したテストケースのみ再実行できるので、全てのテストケースを再実行するというオーバーヘッドを無くすことができます。
実装ポイント
前提
CircleCIのテスト分割機能を利用して、テストの並列化をしています。
こちらの詳細については以下記事で紹介しておりますので、ご興味あれば是非ご覧ください。
環境
- Laravel
- PHPUnit
config.ymlの実装
いきなりですが、以下のコードでRerun failed test
を有効にすることができます。
- テスト実行
- カバレッジレポートをcoverallsへ送信
といったよくあるフローです。
地味にハマりポイントがあったため、順を追って解説していきます。
version: 2.1 orbs: (省略) jobs: test: parallelism: 2 resource_class: large environment: (省略) docker: (省略) working_directory: ~/project steps: - run: name: run parallel test command: | mkdir -p {coverage,test-results} TEST_FILES=$(circleci tests glob "./tests/**/*Test.php") echo "$TEST_FILES" | circleci tests run --command="xargs php .circleci/generate_phpunit_ci_xml.php && \ phpdbg -d memory_limit=-1 -qrr vendor/bin/phpunit \ --order-by=random \ --coverage-clover coverage/coverage-${CIRCLE_NODE_INDEX}.xml \ --log-junit test-results/junit.xml \ --configuration phpunit_ci.xml" --verbose --split-by=timings - run: name: format test result for make timing data command: | if [ -f test-results/junit.xml ]; then sed -i -e "s|/home/circleci/project/||g" test-results/junit.xml else echo "test-results/junit.xmlが出力されていないためスキップします" fi - store_test_results: path: test-results - persist_to_workspace: root: . paths: - coverage report_coverage: docker: - image: cimg/php:8.3 working_directory: ~/project steps: - checkout - attach_workspace: at: . - run: name: install php-coveralls command: | wget https://github.com/php-coveralls/php-coveralls/releases/download/v2.7.0/php-coveralls.phar chmod +x php-coveralls.phar - run: name: send coverage report to coveralls command: | ./php-coveralls.phar -v \ --json_path=coverage/coveralls-upload.json \ --coverage_clover=coverage/coverage-*.xml
circleci tests runを利用する
失敗したテストを再実行できるようにするためには、circleci tests run
コマンドを利用します。
元のテスト実行コマンドである以下を---command
へ渡すように改修します。
xargs php .circleci/generate_phpunit_ci_xml.php phpdbg -d memory_limit=-1 -qrr vendor/bin/phpunit \ --coverage-clover coverage/coverage-${CIRCLE_NODE_INDEX}.xml \ --log-junit test-results/junit.xml \ --configuration phpunit_ci.xml
実装前は以下のようなコードでした。
before
steps: - run: name: run parallel test command: | mkdir -p {coverage,test-results} circleci tests glob "./tests/**/*Test.php" \ | circleci tests split --split-by=timings \ | xargs php .circleci/generate_phpunit_ci_xml.php phpdbg -d memory_limit=-1 -qrr vendor/bin/phpunit \ --coverage-clover coverage/coverage-${CIRCLE_NODE_INDEX}.xml \ --log-junit test-results/junit.xml \ --configuration phpunit_ci.xml
after
steps: - run: name: run parallel test command: | mkdir -p {coverage,test-results} TEST_FILES=$(circleci tests glob "./tests/**/*Test.php") echo "$TEST_FILES" | circleci tests run --command="xargs php .circleci/generate_phpunit_ci_xml.php && \ phpdbg -d memory_limit=-1 -qrr vendor/bin/phpunit \ --order-by=random \ --coverage-clover coverage/coverage-${CIRCLE_NODE_INDEX}.xml \ --log-junit test-results/junit.xml \ --configuration phpunit_ci.xml" --verbose --split-by=timings
上記のようにcircleci tests run
を利用するように実装するだけで、Rerun failed tests
は有効になり、テストが失敗したときにCircleCIのUIでグレーアウトされていた部分が選択できるようになります。
ハマりポイントその1 - junit.xmlが出力されないケースの考慮
並列実行していると失敗したテストケースが1つだった場合、他のexcecutorでは実行するテストケースが0になるため、テスト実行コマンドが実行されないというケースを考慮する必要があります。
例えば、上述のrun parallel test
ステップが実行されないと、test-results/junit.xml
が出力されません。
そうすると、format test result for make timing data
ステップのように、junit.xml
を利用しているステップで以下のようなエラーとなります。
(そもそも大前提としてRerun failed testsではJUnit XML に出力されたテスト結果から再実行するテストケースを判断しているため、JUnit XMLをCircleCIへアップロードすることが必須です。)
なので、以下のようにjunit.xml
の存在チェックをするようにして、無い場合はスキップするように修正しました。(このステップではjunit.xml
に絶対パスが含まれてしまうとCircleCIの解析がうまくいかないため相対パスとなるように置換しています。)
- run: name: format test result for make timing data command: | if [ -f test-results/junit.xml ]; then sed -i -e "s|/home/circleci/project/||g" test-results/junit.xml else echo "test-results/junit.xmlが出力されていないためスキップします" fi
ハマりポイントその2 - テスト再実行時に persist_to_workspace ステップで指定されたディレクトリにコンテンツが見つからず、並行実行が失敗する
テスト実行時のカバレッジレポートのXMLファイル(coverage/coverage-${CIRCLE_NODE_INDEX}.xml
)を、report_coverage
ステップで参照できるようにするために、persist_to_workspace
を利用しています。
これも並列実行していると、テストケースが0のexcecutorではテスト実行コマンドが実行されずcoverage
ディレクトリの中身は空になります。
その後、persist_to_workspace
が実行されると以下のようなエラーで失敗します。
The specified paths did not match any files in /home/circleci/project/coverage
これは、以下のようにmkdir
であらかじめcoverage
のようなディレクトリを作っておきroot
に.
、paths
で作成したディレクトリを指定するとうまくいきます。(参考)
before
- persist_to_workspace: root: coverage paths: - coverage-*.xml
after
- persist_to_workspace: root: . paths: - coverage
成功ログ
まとめ
最後まで読んでいただきありがとうございます…!
実測値としては、このリポジトリでは約2000程のテストケースがあり、普通にRerunすると5分ほどかかっていたものがRerun failed rerun
により約2分ほどで実行完了するようになりました。約60%ほどの速度改善をすることができました。
基本的にcircleci tests run
を経由してテスト実行するようにすれば実現できるので、コスパの良い改善になったかなと思います。是非導入を検討してみてはいかがでしょうか?
CIの改善に少しでもこの記事がお役に立てましたら幸いです。
最後に
ヤプリでは今年も引き続きサーバーサイドエンジニアを募集しています。
興味を持った方、是非一度カジュアル面談を受けてみませんか??