連載
» 2017年07月20日 05時00分 公開

Selenium WebDriverでWebアプリのテストが変わる(後編):Selenium WebDriverのブラウザ自動テストを実践する (3/3)

[後藤正規,ビーブレイクシステムズ]
前のページへ 1|2|3       

非同期処理をテストする

 今までテストコードを書いてきた機能は全て画面遷移を含む同期的なテストでした。しかし、現在Webアプリでは非同期処理を用いて画面遷移せずにページの内容を書き換えることが多くなってきています。

 非同期処理で値を書き変えるような機能に対して、今までのコードでは正しくテストすることができません。非同期処理が実行され、値が変更される前に要素の値が検証されてしまうからです。

 非同期処理をテストするためには、WebDriverWaitというクラスを用いて、明示的に非同期処理が完了するまで待機する必要があります。

 それでは、具体的に「id=button1であるボタンをクリックした後、id=text1であるテキスト部品がhogeとなる」処理について、同期処理と非同期処理のテストコードの違いを見比べてみましょう。

WebElement button1Element= driver.findElement(By.id("button1"));
button1Element.click();
WebElement text1Element = driver.findElement(By.id("text1"));
assertThat(text1Element.getAttribute("value"), is("hoge"));
同期処理の場合
WebElement button1Element= driver.findElement(By.id("button1"));
button1Element.click();
final WebElement text1Element = driver.findElement(By.id("text1"));
// 非同期処理に対応した明示的な待機処理
// WebDriverWaitのコンストラクタの第2引数はタイムアウト秒数
// untilメソッドに待機する条件を指定。
(new WebDriverWait(driver, 10)).until(new ExpectedCondition<Boolean>() {
	public Boolean apply(WebDriver d) {
                // この条件が満たされたら処理が続行される
        return text1Element.getAttribute("value").equals("hoge");
    }
});
assertThat(text1Element.getAttribute("value"), is("hoge"));
非同期処理の場合

 WebDriverWaitを間に差し込む以外は同期処理と非同期処理のテストコードに違いはありません。

 また、「要素の属性が変更した」「要素が表示/非表示となった」といった典型的な判定であれば、ExpectedConditionsというクラスが判定用のメソッドを提供しています。

 先ほどの例であれば、ExpectedConditionsのattributeContainsメソッドを使うことで、以下のように匿名クラスを使わずに記述できます。

 (new WebDriverWait(driver, 10)).until(ExpectedConditions.attributeContains(text1Element, "value", "hoge"));

 ExpectedConditonsに用意されているメソッドは他には以下のようなものがあります。また、第1引数にWebElementを指定しているものは、Byを指定することも可能です。

メソッド 処理概要
attributeContains(WebElement element, String attribute, String value) 要素の属性が指定した値であるかどうか
elementToBeClickable(WebElement element) 要素がvisibleかつenableであるか
visibilityOf(WebElement element) 要素がvisibleか
invisibilityOf(WebElement element) 要素がvisibleでないか
and(ExpectedCondition<?>... conditions) 複数の条件を満たすか
or(ExpectedCondition<?>... conditions) 複数の条件のいずれかを満たすか
not(ExpectedCondition<?> condition) 指定の条件を満たさないか

 なおプログラム中に非同期処理が大量にある場合、上記の明示的な待機がテストコードに多発し、可読性が損なわれる可能性が高くなります。そのようなケースでは、「Selenide」というフレームワークを使うことを検討した方がいいでしょう。Selenideを使うと、同期処理、非同期処理を意識せずに要素のアサーションが行えるようになります。

TIPS「テストコードの保守性を高めるPAGE OBJECTSパターン」

 ここまで記載したテストコードでは、以下のようにテストメソッドに直接xpathで要素を取得するコードを記述していました。

private void editInvalidTest_21char_20char() {
driver.get(initialURL);
        driver.findElement(By.xpath("//table[@id='emptable']/tbody/tr[2]/td/a")).click();

 しかし、このようにテストコードにxpathを埋め込んでしまうと、テストするページのレイアウトに修正が入った場合、そのページをテストするテストコード全てに修正が必要となり、影響範囲が大きくなってしまいます。

 そこで、テストコードの保守性を高めるために、Page Objectsパターンという設計が推奨されています。

 Page Objectsパターンでは、テスト対象の画面に対応するPageクラスを作成します。Pageクラスでは、「名前を入力する」「入力されたデータを新規登録する」といった、その画面の機能に対応した処理をpublicなメソッドで提供するようにします。要素の取得に使うxpathなどはクラスの外に公開しないようにし、画面のレイアウトなどの変更がPageクラスの外に影響しないようにします。

 また、メソッドの返り値はPageクラスとし、メソッドチェインで処理を記述することができるようにします。画面遷移の際は遷移先の画面のPageクラスを返します。

 前ページに記載したeditInvalidTest_21char_20charテストメソッドにPage Objectsパターンを適用したものを見てみましょう。

/**
 * 従業員一覧ページ用PageObject
 */
public class EmployeeSearchPage {
    private WebDriver driver;
    
    public EmployeeSearchPage(WebDriver driver) {
        this.driver = driver;
    }
    
    /**
     * n番目に表示されている従業員の編集ページに遷移する
     * @param n 編集対象の行数(1開始)
     * @return
     */
    public EmployeeEditPage editEmployee(int n){
        getEmployeeEditLinkElement(n).click();
        return new EmployeeEditPage(driver);
    }
    
    private WebElement getEmployeeEditLinkElement(int n){
        return driver.findElement(By.xpath("//table[@id='emptable']/tbody/tr[" + n + "]/td/a"));
    }
}
従業員一覧ページ用のPageクラス
/**
 * 従業員情報変更ページのPageObject
 */
public class EmployeeEditPage {
    private WebDriver driver;
    
    private By employeeIdLabel = By.xpath("//table[@id='edittable']/tbody/tr[1]/td[2]");
    private By nameTextBox = By.xpath("//table[@id='edittable']/tbody/tr[2]/td[2]/input");
    private By orgCombo = By.xpath("//table[@id='edittable']/tbody/tr[3]/td[2]/select");
    private By manRadio = By.xpath("//table[@id='edittable']/tbody/tr[4]/td[2]/input[1]");
    private By womanRadio = By.xpath("//table[@id='edittable']/tbody/tr[4]/td[2]/input[2]");
    private By invalidFlagCheckBox = By.xpath("//table[@id='edittable']/tbody/tr[5]/td[2]/input");
    private By updatebutton = By.xpath("//div[@id='editpanel']/form/input[6]");
    private By errorLabel = By.xpath("//div[@id='editpanel']//ul/li");
        
    public EmployeeEditPage(WebDriver driver) {
        this.driver = driver;
    }
    
    public String getEmployeeId(){
        return driver.findElement(employeeIdLabel).getText();
    }
    
    public EmployeeEditPage setName(String name){
        driver.findElement(nameTextBox).clear();
        driver.findElement(nameTextBox).sendKeys(name);
        return this;
    }
    
    public EmployeeEditPage selectOrg(String text){
        new Select(driver.findElement(orgCombo)).selectByVisibleText(text);
        return this;
    }
 
    public EmployeeEditPage selectMan(){
        driver.findElement(manRadio).click();
        return this;
    }
    
    public EmployeeEditPage selectWoman(){
        driver.findElement(womanRadio).click();
        return this;
    }
    
    public EmployeeEditPage setInvalidFlag(boolean invalidFlag){
        WebElement invalidFlagElem = driver.findElement(invalidFlagCheckBox);
        boolean nowInvalidFlag = invalidFlagElem.isSelected();
        // 現在のフラグの状態が指定の状態と異なればクリックして値を変える
        if(nowInvalidFlag != invalidFlag){
            invalidFlagElem.click();
        }
        return this;
    }
    
    public EmployeeEditResultPage update(){
        driver.findElement(updatebutton).click();
        // 確認ダイアログのOKを押す
        driver.switchTo().alert().accept();
        return new EmployeeEditResultPage(driver);
    }
        
    public String getErrorMessage() {
        return driver.findElement(errorLabel).getText();
    }
}
従業員情報変更ページ用のPageクラス
    public void editInvalidTest_21char_20char() {
        driver.get(initialURL);
        EmployeeSearchPage searchPage = new EmployeeSearchPage(driver);
        EmployeeEditPage editPage = searchPage.editEmployee(2);
        editPage.setName("テスト変更_XX#?1234567890+");
        editPage.selectOrg("開発部1部");
        editPage.selectWoman();
        editPage.setInvalidFlag(true);
        editPage.update();
        assertThat(editPage.getErrorMessage(), is(containsString("社員名は20文字以内で入力してください。")));
    }
Pageクラスを使ったテストメソッド

 どうでしょうか。Page Objectsパターンを用いることで、格段にテストコードが読みやすくなったと思います。また、もしテスト対象の画面の構成が変わったとしても、対応するPageクラスのみの修正で対応できます。テストコードを書く際はぜひPage Objectsパターンを使ってみてください。

テスト自動化は時代の必然。Selenium WebDriverの活用を

 これまでのサンプル実装から、Selenium WebDriverを用いることでWebアプリケーションに対するテストがWebブラウザに依存せず容易に実装できることが分かったと思います。

 近年、業務用のWebアプリケーションをSaaSの形態で提供するなど、Webアプリケーションに対する需要がますます高まってきています。また環境面では、FirefoxやChrome、Internet Explorerなど複数のWebブラウザがシェアを争っている状況もあり、WebアプリケーションはそれぞれのWebブラウザ上において共通的かつ安定した動作が求められます。

 Webアプリケーションの動作保証の手段としては、一般的には複数のWebブラウザにおけるテストの実施ということになるでしょう。

 さらに、Webブラウザのアップデートごとに繰り返し、かつ、Webブラウザごとに似たようなテストを行わなければならないことを考慮すると、「テストは、なるべく自動化する」という結論に達するでしょう。

 Webアプリケーションに対するテスト自動化の実行環境を作成する手段として、Webブラウザに依存する部分を最小限にし、かつ容易にテストプログラムを作成できることが可能なSelenium WebDriverは有力な選択肢として検討に値するのではないでしょうか。

■更新履歴

【2012/11/7】初版公開(後藤正規、株式会社ビーブレイクシステムズ)。

【2017/7/20】2017年の情報に合うように更新(今泉俊幸、株式会社ビーブレイクシステムズ)。


筆者紹介

今泉 俊幸(いまいずみ としゆき)

2011年から、株式会社ビーブレイクシステムズに在籍。

快適に仕事をするためには教育と自動テストこそが大事、という思いを抱き活動中


前のページへ 1|2|3       

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。