Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

CircleCIで失敗したテストケースのみ再実行できるようにする方法

はじめに

こんにちは。サーバーサイドエンジニアの佐野きよ(@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.com

実装ポイント

前提

CircleCIのテスト分割機能を利用して、テストの並列化をしています。
こちらの詳細については以下記事で紹介しておりますので、ご興味あれば是非ご覧ください。

tech.yappli.io

環境

  • Laravel
  • PHPUnit

config.ymlの実装

いきなりですが、以下のコードでRerun failed testを有効にすることができます。

  1. テスト実行
  2. カバレッジレポートを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の改善に少しでもこの記事がお役に立てましたら幸いです。

最後に

ヤプリでは今年も引き続きサーバーサイドエンジニアを募集しています。
興味を持った方、是非一度カジュアル面談を受けてみませんか??

open.talentio.com