CircleCI を使ってテストを自動化していると、テストに失敗したときに、その失敗が例えばコード以外に起因するような場合に、失敗したテストだけを再実行して、迅速にテスト結果を得たい場合があります。このブログでは、失敗したテストのみを実行することが有効な場面や、CircleCI を使って失敗したテストのみを再実行する方法をご紹介します。
失敗したテストの再実行とは?
コードが追加、修正される度に単体テストや結合テスト、E2Eテストを自動実行しているでしょうか? テストの自動実行は、CI (継続的インテグレーション)の要であり、アプリケーションやサービスがリリース、デプロイされている イコール 用意したテストコードが通っている からこそ、安心して新機能の追加やバグの修正が進められます。
別の言い方をすると、コードの追加、修正に問題があるかどうかは、テストの結果によって判断されるべきです。したがって、迅速に開発を進める上では、コードの追加、修正後にできるだけ早く(コードのコミット+プッシュ後、10分以内に)結果が得られることで、開発効率を高めることが可能です。
そのようなニーズに応えるCircleCIの機能が「失敗したテストの再実行(Rerun failed tests)」です。それでは、具体的に使い方を見ていきましょう。
CircleCI を使ったテストの自動実行例
今回は、サンプルプロジェクトとして、https://github.com/mayoct/cci-pytest を使って見ていきます。以前のブログ「テスト分割と並列実行 - 実行時間を短縮する」でご紹介したのと同様のプロジェクト(ただし、言語はJavaからPythonに変更)です。
プロジェクトには、pytest で書かれたテストファイルが全部で10個あり、テスト完了にはそれぞれ、5~100秒の間の時間がかかるように書かれています。
CircleCI を使って、これら10個のテストファイルを(過去のテスト実行時のタイミングデータをもとにして)実行する場合の記述は、次のようになります。
TEST_FILES=$(circleci tests glob "*.py" | circleci tests split --split-by=timings)
pytest --verbose --junitxml=test-results/junit.xml $TEST_FILES
CircleCI でワークフローを実行してみると、10このテストはすべて正常終了します。したがって、画面上の Rerun ボタンをクリックしても、Rerun workflow from failed (失敗したところからテストを再実行する) や Rerun failed tests (失敗したテストを再実行する) はグレーで表示されており、選択できない状態になっています。
それでは、いくつかあるテストファイルのうちの1つ、forty_second_test.py を次のように書き換え、50%の確率で失敗するように書き換えてみます(リポジトリ https://github.com/mayoct/cci-pytest-flaky の forty_second_50_test.py)。
import pytest
import time
import random
def test_():
random.seed()
a = 1
b = random.randint(1, 2) # 50% chance of failure
time.sleep(40)
assert a != b
CircleCI でテストを自動実行し、50%の確率で失敗した場合、再実行するための Rerun ボタンをクリックすると、先ほどとは違い、Rerun workflow from failed (失敗したところからテストを再実行する) は選択可能になっているものの、Rerun failed tests (失敗したテストを再実行) は引き続き選択ができない状態です。Required: add circleci tests run to config.yml (必須: config.yml に circleci tests run を追加) と表示されている通り、失敗したテストを再実行するには、config.yml ファイルに修正を加える必要があるようです。
なお、TESTSタブを見ると、過去の実行結果から(つまり、成功、失敗が不安定であることから)、このテストに対してFLAKYラベルが付与されていることがわかります。
失敗したテストを再実行するためのテスト実行方法
失敗したテストを再実行するには、circleci tests run を追加する旨のメッセージが出力されていましたが、冒頭にあげた config.yml 中の
TEST_FILES=$(circleci tests glob "*.py" | circleci tests split --split-by=timings)
pytest --verbose --junitxml=test-results/junit.xml $TEST_FILES
を具体的には、次のように修正します(リポジトリ https://github.com/mayoct/cci-pytest-flaky_rerun-failed-tests の config.yml) 。
TEST_FILES=$(circleci tests glob "*.py")
echo "$TEST_FILES" | circleci tests run \
--command="xargs pytest -o junit_family=legacy --junitxml=test-results/junit.xml" \
--verbose --split-by=timings
ここでは、テスト実行のための pytest コマンドが、circleci tests run コマンドを経由して呼び出されていることがわかります。
実際に、(50%の確率で)テストに失敗すると、実行結果の画面上では、次のように出力されます。
そして、Rerun ボタンを押した後の表示も、次のように変わり、Rerun workflow from start (ワークフローを最初から再実行する)、Rerun failed tests (失敗したテストを再実行する) も選択可能になっています。失敗したところからワークフローを再実行することで、forty_second_50_test.py だけが再実行されるので(再実行の結果が成功か失敗かは50%の確率ですが)、再実行の結果は10テストをすべて実行する場合にくらべ、大幅に短縮されます(この例では40秒程度)。
さいごに
さて、失敗したテストを再実行する機能が有効に利用できるのは、どのような場面でしょうか? コードの追加や変更に伴うテストに関しては、影響範囲が明確に特定できるのでなければ(また、特定できるように思われても、往々にして想定していない部分で問題が発生することがあります)、テストスイートをすべて再実行するほうが望ましいと言えます。
その一方で、CircleCI のテストインサイトでも検出可能なFlakyな(結果が安定しない)テストがあることも事実です。つまり、コードに何も手を加えていない状態であっても、結果が(成功であれ、失敗であれ)常に同じにならないようなテストです。
たとえば、E2Eテストで、特定の画面の描画や遷移を待って、次の入力などのアクションを行うべきところを、(画面の描画や遷移を検出しないで)一定時間待った上でアクションを実行した場合、(何らかの理由で)画面の描画が追いついていない場合にエラーになるケースが考えられます。たまたまそのタイミングでテスト環境の負荷が高い場合、テスト全体をやり直すのではなく、失敗したテストのみを再実行することで(負荷の低い状態で)テストをパスする、といったことが考えられます(本質的にはリソースクラスの見直しや、テストの方法など見直したほうがよい点はあるにせよ)。
あるいは、バックエンドのデータベースはコンフィグの中で起動、停止するのではなく、別に管理されている場合、データベースが起動していなかったり、アクセスできない状態でテストを実行すれば、データベースアクセスに関わるところはすべてエラーになることが想定されます。そのような場合にも、データベースへのアクセスができることを確認した上で、失敗したテストだけを再実行することが、開発を早く進めていくためには有効かと思われます。
CircleCI を活用することで、テスト結果を無駄に待って貴重な実装時間を失ったり、間違いなく通るテストの実行でツールのコストを消費しないような開発を実現してください。