Selenium 2で非同期処理を待機する5つの方法

リッチなユーザインターフェースを備えたWebアプリケーションでは、Ajaxやアニメーションなどの非同期処理はよく用いられます。こういったWebアプリケーションをSeleniumでテストする際、従来の静的なユーザインターフェースを持つWebアプリケーションと同じようにテストを作成していると、実際にテストを動かした時に次のような問題が発生することがあります。

  • 存在するはずの要素が見つからない(あるいはその逆)
  • 画面全体、もしくは特定の要素の内容が更新されていない

例えば、以下のようなソースコードです。

ajaxButton.click();
WebElement fooElement = driver.findElement(By.id("foo"));

非同期処理を伴うボタンをクリックした後にfooというIDを持つ要素を探していますが、この要素が非同期処理の完了後に表示される要素であった場合、うまく動かないおそれがあります。なぜなら、要素を検索した時点でまだ要素が存在していない可能性があるためです。
こういった問題の原因は、ブラウザが非同期処理の完了を待たず、一続きに操作を実行してしまうことにあります。したがって、こういったWebアプリケーションを正しくテストするためには、何らかの方法で非同期処理の完了を待機する必要があります。
Selenium 2で非同期処理を待機する方法は5つあります。
今回は例として、上記のソースコードを、それぞれの方法に当てはめた形で書き換えてみます。

1. Thread.sleep() を使う

ajaxButton.click();
Thread.sleep(5000);
WebElement fooElement = driver.findElement(By.id("foo"));

最も単純なのはThread.sleep()を使う方法です。実行中のスレッドを一定時間停止することで、非同期処理の完了を待ちます。
しかしこの方法は、Thread.sleep()が終了した時点で非同期処理が完了していることを保証しません。そのため、非同期処理が予想外に長引いてしまった場合にうまくいかない可能性があります。また逆に、非同期処理が一瞬で終わってしまった場合には、無駄な待ち時間が発生してしまいます。
したがって、この方法は本格的な運用では用いるべきではありません。ごく簡単な動作確認や説明のために使用するだけに留めておきましょう。

2. 非同期処理が完了するまでループする

ajaxButton.click();
WebElement fooElement = null;
for (int i = 0;; i++) {
    try {
        fooElement = driver.findElement(By.id("foo"));
        break;
    } catch (NoSuchElementException e) {
        if (i > 30) {
            throw e;
        }
    }
    Thread.sleep(1000);
}

非同期処理の完了が確認できるまでループさせる方法です。この例における完了条件は、非同期処理の実行後に画面内に現れると予想される要素が見つかることです。
ループ内では、まずfindElement()を実行して要素を探します。そこで例外が発生しなかった場合、つまり要素が見つかった場合には、完了条件を満たしたのでループを抜けて待機処理を終えます。例外が発生した場合は、要素が見つからなかったということです。その時はループを抜けずに、1000ミリ秒のインターバル後に再度findElement()を実行します。これを、要素が見つかるまで繰り返します。タイムアウト処理として、iの値が一定値を超えた場合には例外を投げるようにしています。
Thread.sleep()の方法と異なり、この方法ならば待機後に非同期処理が完了していることが保証されます。非同期処理の状態を一定間隔で確認することで、無駄な待ち時間も極力抑えることができます。
基本的にこの方法であらゆる非同期処理のパターンに対処できますが、記述が冗長で、完了条件の判定処理と、インターバルやタイムアウトなどの処理が混在してしまうのが難点です。処理を別クラスに分けて再利用可能な形にしてもいいのですが、それよりも以下の方法を用いるほうが楽です。

3. Waitクラスを使う

Wait wait = new Wait() {
    @Override
    public boolean until() {
        try {
            driver.findElement(By.id("foo"));
            return true;
        } catch (NoSuchElementException ignore) {}
        return false;
    }
};
ajaxButton.click();
wait.wait("Cannot find element!", 30000);
WebElement fooElement = driver.findElement(By.id("foo"));

Waitクラスは、非同期処理の待機を簡単に実現するためのユーティリティクラスです。Seleniumには、こういったクラスがあらかじめ用意されています。
Waitクラスは抽象クラスとして定義されていて、until()をオーバーライドして使用します。オーバーライドしたuntil()の中に非同期処理の完了条件を記述します。メソッドの戻り値の型はbooleanで、非同期処理の完了が確認できた時にtrueを返し、それ以外の時にはfalseを返すように実装します。
このuntil()が実際に呼び出されるのはwait()の内部です。wait()の内部では、until()を実行して、その戻り値を評価し、trueが返却されるまでこれを繰り返すようになっています。なお、このwait()java.lang.Objectに定義されているwait()と名前が同じですが、シグネチャが異なるので注意してください。
このように、Waitクラスを用いれば、自身でループ処理などを記述することなく簡単に非同期処理の完了を待機できます。インターバルやタイムアウトなどの設定もwait()の引数で指定するだけなので、非常に簡潔です。
ただ、Waitクラスは便利なクラスではあるのですが、このクラスはSelenium RCで使用されていた少し古いクラスです。Selenium 2で待機処理を実現する場合、より使い勝手が良くなったWebDriverWaitクラスを使うのがおすすめです。

4. WebDriverWaitクラスを使う

Wait<WebDriver> wait = new WebDriverWait(driver, 30);
ExpectedCondition<WebElement> presenceOfElementIdentifiedAsFoo = new ExpectedCondition<WebElement>() {
    public WebElement apply(WebDriver driver) {
        return driver.findElement(By.id("foo"));
    }
};
ajaxButton.click();
WebElement fooElement = wait.until(presenceOfElementIdentifiedAsFoo);

WebDriverWaitクラスは、上記のWaitクラスと同じく非同期処理を待機するためのユーティリティクラスです。名前の通り、WebDriverクラスとの組み合わせで使用することが想定されています。
使い方としては、ExpectedConditionインターフェースのapply()を実装し、その中に非同期処理の完了条件を記述します。そして、その実装クラスのインスタンスWebDriverWairuntil()に渡します。until()の内部では、以下のいずれかの条件を満たすまで、渡されたインスタンスapply()が繰り返し実行されます。

  • apply()実行時に例外が発生せず、戻り値がnullでない
  • apply()実行時に例外が発生せず、戻り値がBoolean.TRUEと等しい(戻り値の型がBooleanの場合)
  • あらかじめ設定されたタイムアウト時間に達した

タイムアウトの場合は例外が投げられますが、それ以外の場合は正常に非同期処理が完了したとみなされ、apply()の戻り値がそのままuntil()の戻り値として返されます。apply()の戻り値の型はExpectedConditionの型引数として指定したもので、この例の場合はWebElementです。
WebDriverWaitクラスはWaitクラスと比べ、until()の戻り値の型をユーザが決めることができるという柔軟さがあります。また、非同期処理の完了条件の部分を別のクラスへと委譲しているため、それぞれのクラスの再利用性が向上しています。

9/14 追記

Seleniumのバージョン2.6.0から以下のようなクラスが導入されました。

import static org.openqa.selenium.support.ui.ExpectedConditions.*;
// 中略
Wait<WebDriver> wait = new WebDriverWait(driver, 30);
ajaxButton.click();
WebElement fooElement = wait.until(presenceOfElementLocated(By.id("foo"));

ExpectedConditionsクラスは、よく使われる完了条件をまとめたクラスです。条件に応じたExpectedConditionを返してくれるメソッドをまとめて定義していて、上記のようにuntil()の引数に直接メソッドを記述するだけで、簡単に非同期処理の完了を待機できます。また、ExpectedConditionを返すメソッドは全てstaticメソッドなので、このように静的インポートを活用することによって、自然言語的で読みやすいソースコードになります。
ExpectedConditionsクラスには、上記のpresenceOfElementLocated()というメソッド以外にも以下のようなメソッドが用意されています。

  • titleIs()
  • titleContains()
  • visibilityOfElementLocated()
  • visibilityOf()
  • presenceOfAllElementsLocatedBy()
  • textToBePresentInElement()
  • textToBePresentInElementValue()
  • frameToBeAvailableAndSwitchToIt()
  • invisibilityOfElementLocated()
  • elementToBeClickable()
  • stalenessOf()

それぞれのメソッドがどのような完了条件を表しているか、メソッド名から大体わかるとは思いますが、詳しく知りたい場合はExpectedConditionsJavadocを参照してください。

5. implicitlyWait() を設定する

driver.manage().timeouts().implicitlyWait(30, TimeUnit.SECONDS);
ajaxButton.click();
WebElement fooElement = driver.findElement(By.id("foo"));

これまでの方法は全て、待機処理をソースコード内に明示的に記述したものでした。それに対してimplicitlyWait()は、待機処理の暗黙的実行を示すものです。
implicitlyWait()を用いて待機時間を設定すると、要素の検索時にその要素が見つからなかった場合、そこですぐに例外を出さずに、設定された時間だけ要素の出現を待機するようになります。一度implicitlyWait()で待機時間を設定したDriverは、それ以降全ての要素検索で暗黙的に待機を行います。
非常に簡潔に待機処理を実現できるメソッドではありますが、その反面、要素の検索時にしか利用できないという短所もあります。例えば、要素が非表示になるまで待機したり、ページのタイトルが変更されるまで待機するといった場合は、このメソッドでは対応できません。他の方法を用いる必要があります。とはいっても、簡単なWebアプリケーションのテストであれば、この方法で全く問題ないでしょう。

まとめ

以上がSelenium 2で非同期処理を待機する5つの方法です。特別な理由がない限り、4と5の方法を用いるのがベストだと思います。個人的には、明示的に待機処理を記述する4の方法が好みです。
他にもこんな方法があるよ、といったものがあれば是非教えてくださると嬉しいです。