見出し画像

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

リトライの要件は以下としました。

1. テストが失敗したら一定回数リトライする
2. 失敗したテストだけをリトライする
3. リトライで最終的に成功すれば、そのケースはPassしたことにしたい

考えられるテストのリトライの実装方法は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コマンドをたたきます。

画像1

実行結果はこちら(可視化のためにAllureレポートに出力しています)

画像2

おや?
テスト回数は1回だけ。リトライされていないみたいですね。
おかしいな、と思ってレポートの右ペイン「Retries」をクリックしてみると。

画像6

ありましたありました!
ちょうど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回として実行します。

画像4

うまくいったようです。(ConsoleにJUnitの@Testのときのようにログが出ないのが気になりますが)

結果を見てみましょう。

画像5

おや?
リトライ回数分のすべてのケースがレポートに出力されていますね。

失敗したケースは無視されて「Ignored Case」となり、メッセージには「Do not fail completely, but repeat the test」と出ています。

画像6

成功したあとのケースも「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システムのための自動テスト基礎」には不安定なテストへの対処が言及されています。

<<不安定なテストへの対処>>
1. テスト書き直す
2. テストをピラミッドの下の層へ移動させる
3. 価値のないテストとみなし、テストを止める

特に3つ目にはすごく共感しました。Flakyだからといってリトライするのは無意味ですし、ならばいっそやめてしまったほうが良いというものです。

それでもリトライが必要というシチュエーンションもあるかもしれません。そんなときは、まずアンチパターンを把握することをおすすめします。

リトライのアンチパターン

・繰り返し実行してはいけない操作のテスト
 → テストデータの都合上、繰り返せない操作はリトライに不向き
・センシティブなテスト
 → 待機時間などの性能検証もスコープであればリトライはNG
・長いシナリオテスト
 → どこで依存関係が壊れるかわからない
・無限リトライ
 → もはや無法地帯
・全テストリトライ
 → もはや…

こういったケースでは必ずリトライを避けるようにします。
次にルールを定めましょう。

ルール例
・リトライ上限回数を決める
・繰り返しできるテストのみリトライする
・独立性が担保されているテストのみリトライする
・待機時間の追加とリトライの併用は禁止とする

テストのリトライは用法用量をまもって運用しましょう。

まとめ

リトライの実装について淡々とレポートするつもりだったのですが、リトライの危険性を戒めるエントリーになってしまいました。
書いていてモヤモヤが晴れるようで楽しかったです。

皆様も、E2EテストのFlakinessを乗り越えて、より良いテストの旅をつづけられますように。

Happy Test Journey!!

最後に少し宣伝です

弊社のテスト自動化支援サービスでは、テスト自動化のさまざまなノウハウを生かしてお客様のプロジェクトの自動テストの信頼性向上を支援します。
お気軽に弊社の窓口までご相談ください。

テスト自動化支援 サービスのご紹介| 株式会社SHIFT


__________________________________

執筆者プロフィール:森川 知雄
中堅SIerでテスト管理と業務ツール、テスト自動化ツール開発を12年経験。
SHIFTでは、GUIテストの自動化ツールRacine(ラシーヌ)の開発を担当。
GUIテストに限らず、なんでも自動化することを好むが、ルンバが掃除しているところを眺めるのは好まないタイプ。
さまざま案件で自動化、効率化による顧客への価値創出を日々模索している。


公式noteお問合せ画像


「スキ」ありがとうございます!
「無駄をなくしたスマートな社会の実現」を目指し、ソフトウェア製品の開発、運用、マーケティングなど、あらゆる立場から携わるSHIFT Groupの公式noteです。エンタメ業界から、Web系、金融/製造/小売りなどのエンタープライズ業界まで広い知見を活かした情報を発信しています。