garbagetown

個人の日記です

PageFactory

おもむろに以下のページを和訳してみました。

PageFactory

PageObject パターンをサポートするため、WebDriver の support ライブラリにはファクトリクラスが含まれています。

単純な例

PageFactory を使うには、例えば以下のように、まず WebEelemtent か List<WebElement> であるフィールドをいくつか PageObject に宣言します:

package org.openqa.selenium.example;

import org.openqa.selenium.WebElement;

public class GoogleSearchPage {
    // これが element です
    private WebElement q;

    public void searchFor(String text) {
        // そしてここで使います。まるで事前にインスタンス化
        // していないかのように見えることに注意してください....
        q.sendKeys(text);
        q.submit();
    }
}

このコードが、"q" フィールドがインスタンス化されていないことによる NullPointerException が投げられることなく動くようにするために、PageObject を初期化する必要があります:

package org.openqa.selenium.example;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.htmlunit.HtmlUnitDriver;
import org.openqa.selenium.support.PageFactory;

public class UsingGoogleSearchPage {
    public static void main(String[] args) {
        // WebDriver の新しいインスタンスを作ります
        WebDriver driver = new HtmlUnitDriver();

        // 正しい場所に遷移します
        driver.get("http://www.google.com/");

        // 検索ページクラスの新しいインスタンスを作り
        // その中のあらゆる WebElement フィールドを初期化します。
        GoogleSearchPage page = PageFactory.initElements(driver, GoogleSearchPage.class);

        // そして検索を行います。
        page.searchFor("Cheese");
    }
}

説明

PageFactory は気の利いたデフォルトを当てにしています: Java クラス内のフィールド名は、HTML ページ上の要素の "id" または "name" と仮定します。上記の例でいうと:

    q.sendKeys(text);

この行は以下と等価です:

    driver.findElement(By.id("q")).sendKeys(text);

ここで使われている driver インスタンスは、PageFactory の initElements メソッドに渡されたものです。

上記の例では、PageObject の初期化を PageFactory に依存しました。これはまず、"WebDriver" を単一の引数に取るコンストラクタ (public SomePage(WebDriver driver) {) を探すところから行われます。これが存在しない場合、デフォルトコンストラクタが呼ばれます。しかし、PageObject があるひとつの WebDriver インタフェースのインスタンスよりも多くの物に依存する場合があります。このような場合のために、PageFactory を使ってすでに構築されたオブジェクトの要素を初期化することができます。

ComplexPageObject page = new ComplexPageObject("expected title", driver);

// この場合でも、初期化された要素を使うために driver インスタンスを
// 渡す必要があることに注意してください
PageFactory.initElements(driver, page);

アノテーションによる動作例

上記の例を実行すると、PageFactory はクラス内の WebElement の名前とマッチするページ上の要素を探します。まず ID 属性がマッチする要素を探します。これが失敗した場合、PageFactory は "name" 属性の値がマッチする要素を探しに戻ります。

このコードは動作しますが、Google ホームページのソースに馴染みのないひとは、検索フィールドの名前が "q" であることを知らないかもしれません。幸いなことに、アノテーションを使って、意味の分かり易い名前を採用し、要素の検索戦略を変更することができます:

package org.openqa.selenium.example;

import org.openqa.selenium.By;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.How;
import org.openqa.selenium.WebElement;

public class GoogleSearchPage {
    // ここでは name 属性を使って要素を探します
    @FindBy(how = How.NAME, using = "q")
    private WebElement searchBox;

    public void searchFor(String text) {
        // 先ほどと同じように要素を使い続けることができます
        searchBox.sendKeys(text);
        searchBox.submit();
    }
} 

依然として残るひとつの問題は、WebElement のメソッドを呼ぶたびに driver が再び現在のページを訪れて要素を探してしまうことです。AJAX ヘビーなアプリケーションにおいてはこれが望ましいことかもしれませんが、Google 検索ページの場合は要素は常にそこにあり、変更されないことが分かっています。このページを離れることや、戻ってくる (同じ名前の別の要素が存在するかもしれない) ことがないことも分かっています。一度検索した要素を "キャッシュ" できれば便利でしょう:

package org.openqa.selenium.example;

import org.openqa.selenium.By;
import org.openqa.selenium.support.CacheLookup;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.How;
import org.openqa.selenium.WebElement;

public class GoogleSearchPage {
    // ここでは name 属性を使って要素を探しますが、
    // 最初に使用された後は決して検索されません
    @FindBy(how = How.NAME, using = "q")
    @CacheLookup
    private WebElement searchBox;

    public void searchFor(String text) {
        // 先ほどと同じように要素を使い続けることができます
        searchBox.sendKeys(text);
        searchBox.submit();
    }
} 

冗長性の削減

上記の例はまだ少し冗長です。もう少しきれいにアノテーションを使うと以下のようになるでしょう:

public class GoogleSearchPage {
  @FindBy(name = "q")
  private WebElement searchBox;

  // クラスの残りの部分に変更はありません
}

注意

  • PageFactory を使うと、フィールドが初期化されていると見なすことができます。PageFactory を使わない場合、フィールドがすでに初期化されていると見なすと NullPointerException がスローされます
  • List<WebElement> は @FindBy または @FindBys アノテーションを持つ場合のみ装飾されます。ページ上の複数の要素が同じ id や name を持つことはまれなので、WebElement フィールドでは動作するデフォルトの "id または name" 検索戦略をリストに適用することは困難です。
  • WebElements は遅延評価されます。PageObject 内の WebElement フィールドが使われない場合、"findElement" に呼び出されることは決してありません
  • この機能はダイナミックプロキシを使って動作しています。これは、たとえ driver の型を知っていたとしても、WebElement が特定のサブクラスであると期待するべきではないことを意味します。例えば HtmlUnitDriver を使う場合でも、WebElement フィールドが HtmlUnitWebElement インスタンスによって初期化されることを期待するべきではありません。