不安定性への対処
非同期リクエストとそれに伴う動的なページ書き換えが増えると、E2Eの自動テストは不安定になります。例えば、要素取得を行うときに、ページ書き換えが終わっていなくてまだ取得する予定の要素が表示されていないというような、いわゆるタイミング問題が発生するからです。
この問題に対処するためには、人間がほぼ無意識のうちに行っている「要素が表示されるまで待つ」という処理をコードで実装する必要があります。例えば、要素取得であれば、以下のようなラッパーメソッドの作成が考えられます。
WebElement findElement(WebDriver driver, By by) { long timeOutInSeconds = 10; WebDriverWait wait = new WebDriverWait(driver, timeOutInSeconds); wait.until(ExpectedConditions.visibilityOfElementLocated(by)); return driver.findElement(by); }
WebDriverWaitは、Explicit Waitと呼ばれている明示的な待ち処理を実現するためのクラスです。untilメソッドを呼び出すと以下のような処理が行われます。
- 渡された条件を満たすかチェック(今回の場合、要素が存在して見えること)
- 満たしていれば次の処理へ進む(今回の場合、要素の取得)
- 満たしていなければ0.5秒待機して再び1.へ戻る
- 最大待機時間(今回の場合、10秒)を過ぎても条件を満たさなければタイムアウトエラー
ExpectedConditionsは、Seleniumテストでよく使われる待ち条件を集めたクラスです。今回のvisibilityOfElementLocatedメソッドでは、byで取得できる要素がDOM上に存在して、視認可能であることをチェックする条件に指定しています。
Selenideのような一部のSeleniumラッパーライブラリの中には、上記で書いたような待ち処理を内部で自動で行ってくれるものも存在します。上記のような処理を自分で記述して自作フレームワークを構築していくのは手間なので、可能であればそういったライブラリの導入を検討したほうが楽です。
しかし、言語やライブラリの選択によってはそういった機能を得られない場合もあります。ライブラリが提供する機能だけではカバーしてくれない待ち処理も存在するので、上記のような明示的な待ち処理の実装方法は把握しておくことをおすすめします。
ここまで不安定なテストを安定させる方法を書きましたが、どれだけがんばっても不安定なテストを完全になくすことは簡単ではありません。そして、不安定なテストが原因による失敗が常態化していると、本当のバグでテストが落ちてるときも無視されるようになってしまう恐れがあります。この問題を改善するためには、テストが失敗したときに一定回数のリトライを認め、その回数内で成功したらOKとする方法があります。
JUnit 4の場合、次のようなTestRuleを実装したRetryRuleクラスを作成すれば実現可能です。
package com.example.project; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; public class RetryRule implements TestRule { private static final Logger LOG = LogManager.getLogger(RetryRule.class.getName()); private final int maxRetryCount; public RetryRule(int maxRetryCount) { this.maxRetryCount = maxRetryCount; } @Override public Statement apply(Statement base, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { Throwable caughtThrowable = null; for (int i = 0; i < maxRetryCount; i++) { try { base.evaluate(); return; } catch (Throwable t) { caughtThrowable = t; LOG.error(description.getDisplayName() + ": run " + (i + 1) + " failed"); } } LOG.error(description.getDisplayName() + ": giving up after " + maxRetryCount + " failures"); throw caughtThrowable; } }; } }
package com.example.project; import org.junit.Rule; import org.junit.Test; import java.util.Random; public class SampleTest { @Rule public RetryRule retryRule = new RetryRule(3); @Test public void flakyTest() { // 不安定なテスト } }
上記のSampleTestクラスの場合だと、クラス内のすべてのテストメソッドに対してそれぞれ最大3回までリトライするようになり、それまでに1回でも通ればテストが成功したと判定されます。
このように不安定なテストを許容する方法はとても効果的ですが、かといって不安定なテストを放置してしまうと徐々にリトライ回数の上限を超えるようなテストが出てくる恐れがあります。なので、リトライしたテストを後から追跡できるようにしておき、継続的に改善することをおすすめします。