yfj2’s Automatic Web Test Related Blog

yfj2のWEBテスト自動化に関わるブログ

【Geb】ページオブジェクトパターンとは? / What is Page Object pattern ?

【Geb】ページオブジェクトパターンとは? / What is Page Object pattern ?
著者:ふじさわゆうき

この記事では、以下を目的としています。

  • GebというGroovy言語で書かれたブラウザ自動化フレームワークを通してPage Object patternを理解すること
  • GebがPage Object patternをどのようにサポートしているのか理解すること

*Gebの開発環境構築については以下の記事を参照してください

目次

  1. 定義
  2. メリット
  3. WebDriverによるサンプルプログラム
  4. GebによるPage Object patternのサポート
  5. GebによるPage Object patternのサンプルプログラム

1. 定義

以下、https://code.google.com/p/selenium/wiki/PageObjectsを翻訳してみたところ、以下の6原則を守ったものが定義と考えるとわかりやすい。

  1. pulibcメソッドはページの提供するサービスを表すこと
  2. ページの内部を露出させてはならない
  3. 原則としてassertionしてはならない
  4. メソッドのreturnは、他のPageObjectsであること
  5. ページ全体を表す必要はない
  6. 同一アクション(サービス)であったとしても、結果が異なる場合は、異なるメソッドとしてモデル化されていること

Within your web app's UI there are areas that your tests interact with. A Page Object simply model these as objects within the test code.

PageObjects can be thought of as facing in two directions simultaneously.

Facing towards the developer of a test, they represent the services offered by a particular page.

Facing away from the developer, they should be the only thing that has a deep knowledge of the structure of the HTML of a page (or part of a page).

It's simplest to think of the methods on a Page Object as offering the "services" that a page offers rather than exposing the details and mechanics of the page.

As an example, think of the inbox of any web-based email system.

Amongst the services that it offers are typically the ability to compose a new email, to choose to read a single email, and to list the subject lines of the emails in the inbox.

How these are implemented shouldn't matter to the test.

■Summary
・The public methods represent the services that the page offers
・Try not to expose the internals of the page
・Generally don't make assertions
・Methods return other PageObjects
・Need not represent an entire page
・Different results for the same action are modelled as different methods

意訳

ウェブアプリのテストにおいて、テストコードが対話するエリアがある。PageObjectsを利用することでそのようなテストコードをシンプルにモデル化することができる。

PageObjectsは同時に2つの命令に対応していると考えることができる。テスト開発者に対応して、PageObjectsは特定のページによって提供されるサービスを表す。

テスト開発者から離れることで、PageObjectsは、ページ(またはページの一部)の、HTML構造の深い知識を持っている唯一のものでなければならない。

PageObjectsのメソッドは、そのページの詳細と構造を露出するのではなく、そのページの提供する「サービス」を提供するとを考えるのが最もシンプルである。

例として、任意のWebベースの電子メールシステムの受信箱を考えることとする。

そのシステムが提供するサービスは、通常、「一つの電子メールを読むために選択する」、および「受信ボックス内のメールの件名を一覧表示する」、「新しい電子メール」などを構成する機能である。

これら機能のテストがどのように実装されていても、テストが変わるべきではない。

■要約
·pulibcメソッドはページの提供するサービスを表すこと
·ページの内部を露出させてはならない
·原則としてassertionしてはならない
·メソッドのreturnは、PageObjectsであること
·ページ全体を表す必要はない
·同一アクション(サービス)であったとしても、結果が異なる場合は、異なるメソッドとしてモデル化されていること

2. メリット

  • ページのデザイン変更に対してテストシナリオを変更せずに対応することができる
    • 例えば、デザイン変更によりログインページのログインボタンの位置が変わった場合でも、テストシナリオを変更する必要はない。「ログインページ」クラスのログインボタン定義(xpathなど)だけ修正すれば対応することができる
  • テストシナリオの作成者とテストシナリオ実装者を分離することができる
    • 例えば、テストシナリオ者(評価チームなど)は「ログイン⇒商品検索⇒購入⇒購入完了ページ」というシナリオ部分だけ定義する。ログイン処理をどうやって実装するか(WebDriver , Geb)などは、開発者が定義するという分業が可能になる

原文

Page Object reduces the amount of duplicated code and means that if the UI changes, the fix need only be applied in one place.

意訳

PageObjectによって重複コードを減らすことができる。加えて、UIが変更された場合でも、一ヶ所修正すればいいだけで良いことを意味する。

3. WebDriverによるサンプルプログラム

public class LoginPage {
    private final WebDriver driver;

    public LoginPage(WebDriver driver) {
        this.driver = driver;

        // Check that we're on the right page.
        if (!"Login".equals(driver.getTitle())) {
            // Alternatively, we could navigate to the login page, perhaps logging out first
            throw new IllegalStateException("This is not the login page");
        }
    }

    // The login page contains several HTML elements that will be represented as WebElements.
    // The locators for these elements should only be defined once.
        By usernameLocator = By.id("username");
        By passwordLocator = By.id("passwd");
        By loginButtonLocator = By.id("login");

    // The login page allows the user to type their username into the username field
    public LoginPage typeUsername(String username) {
        // This is the only place that "knows" how to enter a username
        driver.findElement(usernameLocator).sendKeys(username);

        // Return the current page object as this action doesn't navigate to a page represented by another PageObject
        return this;    
    }

    // The login page allows the user to type their password into the password field
    public LoginPage typePassword(String password) {
        // This is the only place that "knows" how to enter a password
        driver.findElement(passwordLocator).sendKeys(password);

        // Return the current page object as this action doesn't navigate to a page represented by another PageObject
        return this;    
    }

    // The login page allows the user to submit the login form
    public HomePage submitLogin() {
        // This is the only place that submits the login form and expects the destination to be the home page.
        // A seperate method should be created for the instance of clicking login whilst expecting a login failure. 
        driver.findElement(loginButtonLocator).submit();

        // Return a new page object representing the destination. Should the login page ever
        // go somewhere else (for example, a legal disclaimer) then changing the method signature
        // for this method will mean that all tests that rely on this behaviour won't compile.
        return new HomePage(driver);    
    }

    // The login page allows the user to submit the login form knowing that an invalid username and / or password were entered
    public LoginPage submitLoginExpectingFailure() {
        // This is the only place that submits the login form and expects the destination to be the login page due to login failure.
        driver.findElement(loginButtonLocator).submit();

        // Return a new page object representing the destination. Should the user ever be navigated to the home page after submiting a login with credentials 
        // expected to fail login, the script will fail when it attempts to instantiate the LoginPage PageObject.
        return new LoginPage(driver);   
    }

    // Conceptually, the login page offers the user the service of being able to "log into"
    // the application using a user name and password. 
    public HomePage loginAs(String username, String password) {
        // The PageObject methods that enter username, password & submit login have already defined and should not be repeated here.
        typeUsername(username);
        typePassword(password);
        return submitLogin();
    }
}

4. GebによるPage Object patternのサポート

GebにはPage Object patternをサポートするfirst class objectがある。また、GroovyのDSL機能を活用するためのfirst class objectもある。

■first class objectとは?
「first class object」とは、実行時に生成して利用できるオブジェクトを指す。メソッドの引数として渡したり、変数に代入したり、ほかのオブジェクトに戻り値として返せたりできる。ちなみに、int型やboolean型などJava言語のプリミティブ型は、ファーストクラスオブジェクトとは見なされない。純粋なオブジェクト指向を好む技術者はこれを欠点としてとらえ、Java言語が真のオブジェクト指向言語ではない理由として引き合い出されることもあった。だがGroovyでは、数値を含むすべてをファーストクラスオブジェクトとして実装しており、この欠点を解消している。

Pageクラスは、"location(URL)", "at checker" and "content"を定義している。
開発者は、それらを利用することで実装とサービスを分離することができる。

5. GebによるPage Object patternのサンプルプログラム

5.1 PageObjects適用前

5.1.1 テストシナリオ実行のメインクラス(Browser.drive{...}で記述する)
import geb.Browser
 
Browser.drive {
    go "http://google.com/ncr"
    $("input[name=q]").value "Chuck Norris"
    $("input[value='Google Search']").click()
    waitFor { $("li.g", 0).find("a").text().contains("Chuck") }
}

5.2 PageObjects適用後

  • PageObjectsは以下の2つ
    • GoogleHomePage
    • GoogleResultsPage
5.2.1 テストシナリオ実行のメインクラス
Browser.drive {
    //"location(URL)"の呼び出し
    //GoogleHomePageオブジェクトの呼び出しと
    //GoogleHomePage.urlページへの遷移
    to GoogleHomePage

    //GoogleHomePage.searchを呼び出し
    search "Chuck Norris"

    //"at checker"の呼び出し
    //GoogleResultsPageが表示されることをチェック
    at GoogleResultsPage

    //"content"の呼び出し
    //containsはgroovyの提供するメソッドで、JavaのString.containsと同様
    resultLink(0).text().contains("Chuck")
}
5.2.2 GoogleHomePage(Googleの検索ページ用PageObject)
  • "static url"が"location(URL)"を表している
  • "static at"が"at checker"を表している
  • "static content"が"content"を表している
import geb.*
 
class GoogleHomePage extends Page {
    static url = "http://google.com/?complete=0"
    static at = { title == "Google" }
    static content = {
        //"content"のDSLは以下のように定義する
        //«name» { «definition» }
        searchField { $("input[name=q]") }
        searchButton(to: GoogleResultsPage) { $("input[value='Google Search']") }
    }
 
    void search(String searchTerm) {
        searchField.value searchTerm
        searchButton.click()
    }
}
 
5.2.3 GoogleResultsPage(Googleの検索結果ページ用PageObject)
  • "static url"が"location(URL)"を表している
  • "static at"が"at checker"を表している
  • "static content"が"content"を表している
class GoogleResultsPage extends Page {
    static at = { waitFor { title.endsWith("Google Search") } }
    static content = { 
        //"wait"オプションによって、Googleの検索結果ページに
        //$("li.g")が表示されるまで一定時間待つことができる。
        //もし、表示されなかった場合は"RequiredPageContentNotPresent" 例外が
        //発生する
        results(wait: true) { $("li.g") }
        result { index -> results[index] }
        resultLink { index -> result(index).find("a") }
    }
}