テストを科学する

今回は、前々回に引き続き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日目の記事です。

12月1日に開催された「システムテスト自動化カンファレンス2013」で15分ほどの発表をしてきましたのでこちらでも資料を公開します。
発表に際しては、なるべく現場の生々しい課題を感じて頂けるように心がけました。

カンファレンス自体はテスト自動化に関する概論から始まり各種事例・無償/有償ツールの紹介・テスト設計自動化の手法・組み込みテストの自動化など様々なトピックにわたり非常に盛り上がりました。特に最後のテスト自動化の歴史とこれからに関する講演では、ソフトウェアのテストというものが始まった瞬間から既にテスト自動化の概念が存在したという印象的なお話を聞くことができました。

各発表者のスライドは順次イベントサイトに掲載されていく予定ですので、ご興味のある方はぜひチェックしてみてください。

今回は、Selenium2(WebDriver)でテストケースを作成するときに知っていると便利な「PageObjectデザインパターン」をご紹介します。

こんな方におすすめ

SeleniumはWebブラウザ上で動作するアプリケーションの自動テストツールの中では圧倒的な知名度のあるツールです。2011年にSelenium2がリリースされてからは大きくアーキテクチャが変わり使い勝手も向上しましたが、実際に大量のテストケースを作成・保守するとなると書き方に工夫をする必要があります。

PageObjectデザインパターンの考え方は、
・WebDriverでテストを自動化しているけど、画面変更にテストケースの修正が追いつかず困っている
・WebDriverでテストを自動化したいけど、どのようにテストケースを作成すれば良いのかわからない
という方々にとってとても参考になります。

PageObjectとは?

PageObjectデザインパターンとは、アプリケーションの画面を1つのオブジェクトとしてとらえるデザインパターンの1種のことです。Seleniumの公式サイトでも推奨されている、保守性の高いテストコードの書き方です。

公式サイトに記載されているPageObjectの原則は以下のようなものです。

・The public methods represent the services that the page offers(publicメソッドは、ページが提供するサービスを表す)
・Try not to expose the the internals of the page (ページの内部を公開しないこと)
・Generally don’t make assertions (原則としてassertionを行わないこと)
・Methods return other PageObjects (メソッドは他のPageObjectsを返す)
・Need not represent an entire page (ページ全体を表す必要はない)
・Different results for the same action are modelled as different as different methods (同じアクションに対して異なる結果となる場合には異なるメソッドとしてモデル化する)



この考え方の利点は、テスト対象となるアプリケーションのレイアウトが変更されてもテストコードの変更は最小限で済むということです。

実際のコードを見てみましょう

PageObjectデザインパターンでテストコードを記述するには、コードを大まかに2種類に分割します。

・画面を操作するためのPageクラス
  ・画面やダイアログの単位で作成
  ・ボタンやテキストフィールドなどのHTML要素を保持
  ・各要素を操作するためのAPIを定義
・Pageクラスを操作してテストシナリオを記述するクラス
  ・原則としてPageクラスのAPIだけを呼ぶ
  ・直接HTML要素を操作しない

以下はPageObjectを利用したテストコードの書き方の例となります。
ECサイトで商品の購入を行うテストを書く場合を考えてみましょう。

◆Pageクラス

public class 商品検索ページ {
  WebDriver driver;
  // ページ内の要素を定数として定義
  By textKeyword = By.name("keyword");
  By buttonSearch = By.id("search");

  public 商品検索ページ(WebDriver driver) {
    this.driver = driver;
  }

  // そのページで行える動作をメソッドとして定義
  public 商品検索ページ 商品検索を行う(String keyword) {
    driver.findElement(textKeyword).sendKeys(keyword);
    driver.findElement(buttonSearch).click();
    
    return this;
  }

  public 商品詳細ページ 商品詳細ページに遷移する(String name) {
    // (詳細ページに遷移する処理)
    return new 商品詳細ページ(driver);
  }
}

public class 商品詳細ページ {
  WebDriver driver;

  public 商品詳細ページ(WebDriver driver) {
    this.driver = driver;
  }

  public 商品詳細ページ 商品をカートに入れる() {
    // (カートに入れる処理)
    return this;
  }

  public 購入確認ページ 購入確認画面に遷移する() {
    // (画面遷移の処理)
    return new 購入確認ページ(driver);
  }
}

◆テストシナリオ

@Before
public void シナリオ実行前に必要な処理() {
  (new ログイン画面(driver))
  .ログインする()
  .商品画面に遷移する();
}

@Test
public void 購入フローのテスト() {
  (new 商品画面(driver))
  .商品検索を行う("商品名")
  .商品詳細画面に遷移する()
  .商品をカートに入れる()
  .購入確認画面に遷移する()
  .お届け先を選択する(自宅)
  .支払い方法を選択する(代引)
  .注文確認画面に遷移する()
  .注文終了後にホーム画面に遷移する();
}

PageObjectデザインパターンのメリット

この例のようにテストコードをページとシナリオの二層に分けることで、2つのメリットが得られます。

1つは、メソッドチェーンを使うことでシナリオの可読性が上がる点です。
余計なコードがなく、必要な動作が続けて記載されているためどのようなテストをしているのかを把握しやすくなります。
サンプルのPageクラスの方の実装を見ると分かりますが、
 ・画面遷移が発生する場合は遷移先の画面のオブジェクトを返却
 ・画面遷移が発生しない場合は自分自身の画面のオブジェクト(this)を返却
することでスムーズにメソッドをつなぐことができます。

もう1つのメリットは、アプリケーションのレイアウト変更に強いことです。
たとえば商品検索ページの検索ボタンの位置が変わった場合、修正を行うのは「商品検索ページ」クラスの要素の定義部分だけで良く、テストシナリオには変更を加える必要がなくなるため、変更箇所を最低限に抑えることができます。
また、各画面をクラスとして定義しているため、レイアウト変更があった際にどの部分を修正すれば良いのかを容易に判断することができます。


今回は、PageObjectという考え方を使ってWebDriverのテストコードの可読性・保守性を向上させる方法をご紹介しました。今後もより便利なWebDriverの使い方などご紹介していきます。

Mirosoft PowerShellは.NETやCOMのオブジェクトをPowerShellとオブジェクトとして扱えるということを以前お話しました。Microsoft Excelもワークシートの操作や関数などあらゆるAPIをCOMインターフェースとして公開しています。

そこで今回はExcelをPowerShellから使ってみましょう。

平均値を求めるExcelの関数をAverageを使ってみます。

C:\> Get-Content .\Run-ExcelFunction.ps1

$xl = New-Object -ComObject Excel.Application

$wf = $xl.WorksheetFunction

try {

$wf.Average(@(1, 2, 3, 4, 5))

} finally {

    $wf, $xl | ForEach {

    [void][Runtime.Interopservices.Marshal]::ReleaseComObject($_)

    }
}

C:\> .\Run-ExcelFunction.ps1
3

 
殆どはCOMオブジェクトの準備と破棄で、実体は下記の1行です。WorksheetFunctionオブジェクトを取得できれば、以後は簡単にExcelの関数をPowerShellから呼ぶことができます。

$wf.Average(@(1, 2, 3, 4, 5))

 
全体をtry, finallyでくくっているのは、処理が失敗した場合でも生成したExcelオブジェクトを破棄できるにするためです。

任意の関数を呼べるように、Excel関数の呼び出し部分をスクリプトブロック化してみましょう。以降はコードのみ示します。

$RunExcelFunction = {

    param([scriptblock] $script = {param($xl) });

    $xl = New-Object -ComObject Excel.Application

    $wf = $xl.WorksheetFunction

    try {

    & $script $wf

    } finally {

        $wf, $xl | ForEach {

    [void][Runtime.Interopservices.Marshal]::ReleaseComObject($_)

        }

    }

}

$average = {
    $wf.Average(@(1, 2, 3, 4, 5))
}

$median = {
    $wf.Median(@(1, 2, 3, 4, 5))
}

& $RunExcelFunction $average
& $RunExcelFunction $median

 
開始終了の定型処理がスクリプトブロックになって任意のExcelの関数が簡単に呼び出せるようになりましたね。

さらに、呼び出し側のスクリプトブロックも関数化して、コマンドレットのように引数を撮って呼び出せるようにしてみましょう。引数をパイプラインからも受け取れるようにしてみました。

$RunExcelFunction = {

    param([scriptblock] $script = {param($xl) });

    $xl = New-Object -ComObject Excel.Application

    $wf = $xl.WorksheetFunction

    try {

    & $script $wf

    } finally {

        $wf, $xl | ForEach {

    [void][Runtime.Interopservices.Marshal]::ReleaseComObject($_)

        }

    }

}

function Average {

    param(
        [Parameter(ValueFromPipeline = $true) ]

    $array)

    $average = {
    param($wf)

    $wf.Average($array)

    }.GetNewClosure()

    & $RunExcelFunction $average

}

function Median {

    param(
    [Parameter(ValueFromPipeline = $true) ]
    $array)

    $median = {
    param($wf)

    $wf.Median($array)

    }.GetNewClosure()

& $RunExcelFunction $median

}

,@(1, 2, 3, 4, 5) | Average
,@(1, 2, 3, 4, 5) | Median

 
このようにPowerShellからExcelを始めとしたCOMオブジェクトのインターフェースを利用することは非常に簡単です。

今回のようにExcelなど豊富な統計解析のAPIをもつソフトウェアをPowerShellから利用できれば、PowerShellの標準コマンドレットにない機能でも簡単に自動化できますね。是非、色々なAPIを調べてみて、PowerShellから利用してみてください。

シフトは第三者検証の会社なのでPowerShellでもテストの話をしてみたいと思います。

PowerShellで使用できるテスティングフレームワークはいくつかあるのですが、その中でも先日紹介したChocolatecyからインストールできるBDDフレームのPesterを紹介します。

BDDはBehavior Driven Delopment(振る舞い駆動開発)と呼ばれるもので、TDD(テスト駆動開発)の概念を発展させ、ソフトウェアやAPIの外部から見た振る舞いと仕様を規定していきながら開発を進めていく手法です。

Chocolatecyから簡単にインストールできます。PowerShell ISEから使用できるようにするIsePesterも一緒に入れてしまいましょう。

C:\> 'pester', 'IsePester' | % { cinst $_ }

なぜかパスが追加されないので、$Profileに追加しておきます。

$Env:Path += ";C:\Chocolatey\lib\pester.2.0.3\tools\bin\";

インストールできたら、サンプルにある以下のテストコードを実行してみましょう。

C:\Chocolatey\lib\pester.2.0.3\tools\Examples\Calculator> pester.bat .\Add-Numbers.Tests.ps1
Executing all tests in C:\Chocolatey\lib\pester.2.0.3\tools\Examples\Calculator\Add-Numbers.Tests.ps1
Describing Add-Numbers
[+] adds positive numbers 106ms
[+] adds negative numbers 1ms
[+] adds one negative number to positive number 1ms
[+] concatenates strings if given strings 8ms
[+] should not be 0 4ms
Tests completed in 122ms
Passed: 5 Failed: 0

さらにISEを起動してテストコードを編集する場合、IsePesterをインストールしていれば、Ctrl-F5でPesterが実行されるようになるので、テストコードを書いて、対象コードを書いての繰り返しではISEから実行するのが便利です。

テストコードの中身は以下の通りです。

$here = Split-Path -Parent $MyInvocation.MyCommand.Path
. "$here\Add-Numbers.ps1"

Describe -Tags "Example" "Add-Numbers" {

    It "adds positive numbers" {
        Add-Numbers 2 3 | Should Be 5
    }

    It "adds negative numbers" {
        Add-Numbers (-2) (-2) | Should Be (-4)
    }

    It "adds one negative number to positive number" {
        Add-Numbers (-2) 2 | Should Be 0
    }

    It "concatenates strings if given strings" {
        Add-Numbers two three | Should Be "twothree"
    }

    It "should not be 0" {
        Add-Numbers 2 3 | Should Not Be 0
    }
}

最初の2行でテスト対象のコードを読み込みます。

残りの行がPesterにおけるテストコードになります。パイプを使って、結果を渡して評価しているのがPowerShellらしいですね。

Describe, It, Should, Not, BeなどはPesterがテストのために用意した関数でこのように特定目的(ここではテスト)のために用意した言語をDSL(Domain Specific Language)と呼びます。今回のようにDSLがそのまま処理言語に載るものを特に内部DSLと呼びます。

その他、テストで使える関数は以下のフォルダにあるので、参考にしながら実際に自分のテストを書いてみてください。

C:\Chocolatey\lib\pester.2.0.3\tools\Functions

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

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