テストを科学する

2013/12/05

Selenium WebDriverのwaitを活用しよう

今回は、前々回に引き続きSelenium WebDriverの機能を紹介します。テーマは、UIの自動テストをしたことのある方なら誰でも悩んだことがあるであろう「wait」についてです。

こんな方におすすめ

Seleniumを使って自動テストを作成していると、「自動記録させたテストコードが速く動きすぎて実行時にアプリケーションが追いつかずエラーになる」ということが本当によく発生します。
特に、最近のWebアプリケーションはJavaScriptを使用した動的な要素の変更を伴うものがほとんどなので、実行時にきちんと対象要素が出てくる/消えるまで待機するという制御は必須と言っても良いでしょう。

この記事では、WebDriverで動的な要素を安定的にテストしたい人向けに待機処理の基本をご紹介します。

WebDriverのwait機能

WebDriverのwait機能は、大きく
 ・暗黙的な待機(Implicit Wait)
 ・明示的な待機(Explicit Wait)
の二種類に分かれます。

暗黙的な待機は、テストケース個別で待機処理を記述しなくても予め設定した一定時間の待機を挟んでくれる仕組みです。
一方、明示的な待機は特定の箇所で指定した条件を満たすまで待機するという処理になります。

いずれも、「操作対象の要素が見つかった」など期待する条件が満たされたらすぐ完了する仕組みになっていますので「待機時間を入れすぎてテストコードの実行時間が肥大化してしまう」という心配はありません。期待する状態になるまでにどれくらい待機が必要かということはネットワーク環境やDBのレスポンス等で大きく変わってきますので、くれぐれも「5秒間スリープする」と言った固定値の待機処理は書かないようにしましょう。

では、それぞれ実際にJavaのコードで見てみましょう。

暗黙的な待機(Implicit Wait)

暗黙的な待機は、WebDriverのインスタンスに対して一度設定しておくだけなので簡単に使うことができます。
「ある要素を見つけるとき」、つまりfindElement()もしくはfindElements()を呼んでいるときに限り一定の時間まで自動的に待機します。設定した最大時間まで待っても要素が現れない場合は、エラーとなります。


// 初期設定(一度だけでOK)
WebDriver driver = new FirefoxDriver();
driver.manage().timeouts().implicitlyWait(5, TimeUnit.SECONDS);

// 個々のテストケース(特に記述を加える必要は無し)
WebElement element = driver.findElement(By.id("some_button"));

上の例では、要素を見つけるまで最大5秒待つという設定をしています。

ちなみに、既に表示されている要素についてテキスト等が変更されることをテストしたい場合には暗黙的な待機は使うことができません。


// 暗黙的な待機では解決できないケース
driver.findElement(By.id("register")).click();
assertThat(driver.findElement(By.id("message")).getText(), is("登録しました"));

この例では、ボタンを押下して何らかのデータを登録して処理が完了するとメッセージ領域に「登録しました」という文言が出ることを確認しようとしています。
ボタンを押下したときにメッセージ領域のHTMLが存在しない場合は問題ありませんが、最初から領域が存在してテキストだけが変わるという場合は問題です。
上のテストコードは
 ・登録ボタンを押下する
 ・メッセージ領域のテキストが変化する
 ・変化したテキストが正しいことを検証する
という手順を想定していますが、実際には
 ・登録ボタンを押下する
 ・即座にメッセージ領域のテキストを検証する
 ・まだ変化が完了しておらず、期待値と異なるためエラー
という結果になってしまいます。

このようなケースでは、次の「明示的な待機」を使う必要があります。

明示的な待機(Explicit Wait)

明示的な待機はテストケース内の個別の箇所に対して好きな条件で待機を指定する機能です。この機能を使う場面はおおまかに二種類に分かれます。
 1. 「要素が現れる」以外の条件で待機したい場合
 2. 待機時間を個別に設定したい場合

「要素が現れる」以外の条件で待機したい場合

暗黙的な待機の最後の例にあったような「テキストの内容が変更になるまで待つ」といった条件を使いたい場合の例をご説明します。
明示的な待機には、WebDriverWaitというクラスを使用します。最大待機時間はこのクラスのインスタンスに対して1つ指定できます。

WebDriver driver = new FirefoxDriver();
// 第二引数で最大の待機時間(秒)を設定
WebDriverWait wait = new WebDriverWait(driver, 5);

WebDriverWaitクラスを実際に使用する箇所は以下のようになります。


By buttonRegister = By.id("register");
By textMessage = By.id("message");

driver.findElement(buttonRegister).click();
// 要素のテキストが変更されるまで待つ
wait.until(ExpectedConditions.textToBePresentInElement(
            textMessage, "登録しました"));
assertThat(driver.findElement(textMessage).getText(), is("登録しました"));

untilメソッドに対して待機条件を設定することで、その条件が満たされるまでWebDriverが待機します。待機条件はExpectedConditionsクラスでいくつか既定のものが用意されているので、これを使います。上の例では対象の要素がクリック可能になるまで待機しています。untilメソッドは
 ・予め設定した最大待機時間が経過した場合(エラー終了)
 ・設定した待機条件が満たされた場合(正常終了)
のどちらかの条件で完了します。

ExpectedConditionsクラスで用意されている条件には他にも以下のようなものがあります。
 ・titleContains(String) : タイトルがある文字列を含む
 ・presenceOfElementLocated(Locator) : HTML要素が現れる(見えているかどうかは無関係)
 ・visibilityOfElementLocated(Locator) : HTML要素が可視状態になる
 ・elementToBeClickable(Locator) : HTML要素がクリック可能になる

待機時間を個別に設定したい場合

暗黙的な待機ではWebDriverのインスタンスに対して待機の最大値が1種類だけ設定されるため、特定の要素だけ長く/短く待つといったことはできません。
そこで、例えばファイルアップロードや複雑な検索処理など予め応答時間が長くかかりそうだと分かっているものは個別に待機時間を設定することができます。

上でもご紹介したように待機時間はWebDriverWaitクラスのインスタンス毎に設定されるので、長く待たせたい箇所は別途専用のインスタンスを作ると良いでしょう。

WebDriver driver = new FirefoxDriver();
// アップロードは10秒以内に終了して欲しい
WebDriverWait waitForUpload = new WebDriverWait(driver, 10);
// 検索は8秒以内に終了して欲しい
WebDriverWait waitForSearch = new WebDriverWait(driver, 8);

// 検索機能の呼び出し
By buttonSearch = By.id("search");
By textResult = By.id("result_count");

driver.findElement(buttonSearch).click();
waitForSearch.until(ExpectedConditions.presenceOfElementLocated(textResult));
assertThat(textResult.getText(), is("20件"));

待機条件のカスタマイズ

明示的な待機では様々な条件で待機を制御することができますが、アプリケーションの動きによっては予め用意された条件だけではうまく表現できない場合があります。そのような場合は、自分で待機条件を実装することもできます。
たとえば、元々空だった要素内のテキストが空でなくなるまで待機するという条件を設定したいとします。待機条件を実装するには、ExpectedConditionクラスを継承した無名クラスを作ってapply()メソッドを実装します。この中で、条件が満たされたときにtrueを返すように実装を加えてあげればOKです。


wait.until(new ExpectedCondition<Boolean>() {
  public Boolean apply(WebDriver _driver) {
    // 要素のテキストの長さをチェック
    boolean b = _driver.findElement(locator).getText().length() > 0;
    return Boolean.valueOf(b);
  }
});

厳密には返り値のクラス(=ExpectedConditionクラスに渡す型引数)はBooleanでなくても良く、nullとBoolean.FALSE以外の値を返せば条件を満たしたという判定がされます。applyメソッドの返り値はそのままWebDriverWait#untilメソッドの返り値になるので、何らかの返り値が欲しいときにはBooleanクラス以外を使っても良いでしょう。

まとめ

WebDriverで安定したテストを動かすための待機処理について基本をご紹介しました。暗黙的待機/明示的待機を必要に応じてうまく使い分けることで、煩雑になり過ぎず、かつ安定したテストコードを書くことができます。また、待機条件のカスタマイズをうまく利用することでアプリケーション固有の複雑な待機も簡単に扱うことができます。ぜひ、活用してみてください。

次回はテスト実行後のエラー解析に便利な手法をご紹介する予定です。


この記事は、Selenium Advent Calendar 2013の5日目の記事です。

ソフトウェアテストに関するお悩みなど、まずはお気軽にお問い合わせください。

  • お問い合わせフォーム【お問い合わせはコチラ】
  • 電話でのお問い合わせ【0120-142-117】