FlakyなE2Eテストをリトライで解決する
こんにちは、自動化エンジニアの森川です。
今日は、FlakyなE2Eテストをリトライで解決する方法について考えてみたいと思います。
Flakyなテストとは
E2E自動テストの悩みの種のひとつに「Flakyなテスト」というものがあります。
「Flakyなテスト」は不安定なテストという意味でとらえることもありますが、「不安定なテスト結果」が正しいそうです。※1
テストだけに原因があるとはかぎらず、テスト実行環境や前後の関係も含めて原因として考えられるので、そこも含めて見ていきましょう、ってことだと思っています。
たしかに、E2Eテストでは動いている箇所が多く、サーバの状態やネットワークの状態など不安定な原因がいくつも考えられます。
不安定なテストが発生した場合、対処療法的にリトライするのではなく、根本的な原因の調査と改善が必要なのですが、ここでは一旦置きます。リトライのリスク、怖さについては後ほど述べます。
※1 「初めての自動テスト Webシステムのための自動テスト基礎」より
リトライの実装
筆者のテスト環境はJavaでビルドツールはGradle、テストフレームワークはJUnitでCIツールにはJenkinsを使います。
検証環境
・Java11
・Gradle6.8
・JUnit5.x
・CI(Jenkins)
・Report Tool:Allure-Plugin
リトライの要件は以下としました。
考えられるテストのリトライの実装方法は3つほどあります。
・Jenkinsパイプライン スクリプトのリトライブロックを使う
・Gradleのリトライ プラグインを使う
・JUnitでリトライする
※ 自前でリトライ機能を書くという選択肢もありますが、なるべく既存のオープンソースなライブラリを利用したいので割愛しています。
サンプルテスト
乱数が5で割り切れたらテスト成功という、非常に単純な不安定テストです。
package yo.ur.package;
import java.util.Random;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class RetryByGradle {
@org.junit.jupiter.api.Test
public void test() {
assertEquals(0,new Random().nextInt() % 5);
}
}
Jenkinsパイプラインスクリプトのリトライブロックを使う
まず1つ目。Jenkinsパイプラインスクリプトのリトライ機能は、特定のテストだけをリトライするのには向いていませんでした。
テストが失敗した場合にテスト全体を再実行することになるからです。
これでは2つ目の要件「失敗したテストだけをリトライする」にマッチしません。
成功したテストだけを除くようにして続行させるなど、理論上では実装可能でしょうが、うまいやり方とはいえません。今回は却下しました。
Pipeline: Basic Steps
Gradleのリトライ プラグインを使う
2つ目のGradleのプラグイン「gradle-retry-plugin」はどうでしょうか。
本家のサイトを参考にして進めてみましょう。
build.gradleに定義を追記します。
buildscript {
dependencies {
classpath "org.gradle:test-retry-gradle-plugin:1.2.0"
}
}
...
apply plugin: "org.gradle.test-retry"
...
test {
useJUnitPlatform {
includeEngines 'junit-jupiter'
retry {
// リトライは4回
maxRetries = 4
// テストクラスを指定
filter {
includeClasses.add("*RetryByGradle")
}
}
}
}
コンソールでgradlewコマンドをたたきます。
実行結果はこちら(可視化のためにAllureレポートに出力しています)
おや?
テスト回数は1回だけ。リトライされていないみたいですね。
おかしいな、と思ってレポートの右ペイン「Retries」をクリックしてみると。
ありましたありました!
ちょうど4回リトライしたようですね。
実行日時とエラーメッセージまで表示してくれています。
このメッセージをクリックすると詳細レポートページに飛んでStack and Traceも見ることができます。さすがAllure Reportさん。
ひとつ問題があるといえば、このプラグインはリトライ毎にThreadが立ち上がるので、前回の実行情報を参照できないことです。テストが失敗したときに吐かれるException情報を保持しておいて、リトライの可否判断に使えたらなぁ、なんてことを考えていたので少しだけ残念な感じです。
JUnitの3rd party ライブラリrerunner-jupiterを使う
JUnit5のExtensionライブラリrerunner-jupiterでJUnitによるリトライを試してみます。
https://github.com/artsok/rerunner-jupiter
JUnit5本家にはRepeatedTestというアノテーションがありますが、これは単に繰り返し実行するだけで条件をつけることができません。ググってみると自前でExtentionをこさえている方もおられましたが、ここはGitHub Star数70を信じて採用させていただくことにします。
まず、build.gradleに依存関係を追加します。
testCompile "io.github.artsok:rerunner-jupiter:2.1.6"
io.github.artsok.RepeatedIfExceptionsTestアノテーションを使って、テストメソッドごとに指定します。テストクラスには定義できないので注意です。
package your.lovely.package;
import io.github.artsok.RepeatedIfExceptionsTest;
import java.util.Random;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class RetryByJUnit {
@RepeatedIfExceptionsTest(repeats = 4)
public void test() {
assertEquals(0,new Random().nextInt() % 5);
}
}
同じくリトライ回数を4回として実行します。
うまくいったようです。(ConsoleにJUnitの@Testのときのようにログが出ないのが気になりますが)
結果を見てみましょう。
おや?
リトライ回数分のすべてのケースがレポートに出力されていますね。
失敗したケースは無視されて「Ignored Case」となり、メッセージには「Do not fail completely, but repeat the test」と出ています。
成功したあとのケースも「Ignored Case」でしたが、メッセージは「Turn off the remaining repetitions as the test ultimately passed」と出るようです。
レポートの右ペイン「Retries」をクリックしてみましたが、何もでてきませんでした。
Gradle Pluginとはちがってテストケースを動的に増殖させているようなイメージですね。
このrerunner-jupiterというJUnit Extensionでは、リトライを無視する例外クラスの指定や、最小テスト成功回数、リトライ時に一定時間待機なんていうオプションパラメータ指定できます。
詳しくはこちらをご覧ください。
rerunner-jupiter/README.md at master · artsok/rerunner-jupiter
プチまとめ
Allure Reportとの連携など考慮するとテストケース数に変化を与えないGradle Pluginが良いですし、例外クラスの指定などのきめ細かな条件を指定したいのならばJUnitのrerunner-jupiterエクステンションがよさそうです。
要件によって使い分けるのが良いと思います。
ちなみになりますが、Fail時の例外処理を監視して、同一Exception(+Message)が一定回数続いたらリトライを停止する処理を追加したbranchはこちらです。
リトライがよろしくない理由
おかげさまで最適なリトライの実装方法を知ることができましたが、ここでどうしてFlakyなテストをリトライするのが、よろしくないのかを考えてみましょう。
そもそもFlakyな原因はネットワークやサーバレスポンスの遅延などさまざまです。ですが、後者の場合はサービスの性能に問題がある可能性が潜んでいますし、よくある画面表示の待機TimeOutでFailする場合は、クライアント側のJavaScriptの処理に問題がバグや性能が悪化している可能性もあります。
リトライを実装するということはこれらの事象を報告せず、調査せず、隠していることになりかねません。
それはよろしくないことです。
とはいえ、調査する時間も取れない(テスト自動化の見積で「Flaky対策」工数を確保するのは現実世界では困難)と思います。
ではどうすればよいでしょう?
名著「初めての自動テスト Webシステムのための自動テスト基礎」には不安定なテストへの対処が言及されています。
特に3つ目にはすごく共感しました。Flakyだからといってリトライするのは無意味ですし、ならばいっそやめてしまったほうが良いというものです。
それでもリトライが必要というシチュエーンションもあるかもしれません。そんなときは、まずアンチパターンを把握することをおすすめします。
リトライのアンチパターン
こういったケースでは必ずリトライを避けるようにします。
次にルールを定めましょう。
ルール例
・リトライ上限回数を決める
・繰り返しできるテストのみリトライする
・独立性が担保されているテストのみリトライする
・待機時間の追加とリトライの併用は禁止とする
テストのリトライは用法用量をまもって運用しましょう。
まとめ
リトライの実装について淡々とレポートするつもりだったのですが、リトライの危険性を戒めるエントリーになってしまいました。
書いていてモヤモヤが晴れるようで楽しかったです。
皆様も、E2EテストのFlakinessを乗り越えて、より良いテストの旅をつづけられますように。
Happy Test Journey!!
最後に少し宣伝です
弊社のテスト自動化支援サービスでは、テスト自動化のさまざまなノウハウを生かしてお客様のプロジェクトの自動テストの信頼性向上を支援します。
お気軽に弊社の窓口までご相談ください。
テスト自動化支援 サービスのご紹介| 株式会社SHIFT
__________________________________
お問合せはお気軽に
https://service.shiftinc.jp/contact/
SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/
SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/
SHIFTの導入事例
https://service.shiftinc.jp/case/
お役立ち資料はこちら
https://service.shiftinc.jp/resources/
SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/