garbagetown

個人の日記です

Page Objects

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

PageObjects や Page Object, Page Objects の表記揺れは原文ママです。それと、原文の文法がところどころ怪しくて読みづらかったので誤訳があるかもしれません。ご指摘等ある方はコメント欄またはツイッターにてよろしくお願いいたします。

Page Objects

web アプリケーション UI の中に、テストと関わり合うエリアが存在します。Page Object は、テストコード内においてこれらをオブジェクトとしてシンプルにモデル化します。これは重複するコードの量を減らし、そして UI が変更された際に、その修正を適用する箇所がただ一箇所になることを意味します。

実装上の注意

PageObjects は同時に二つの面を向けていると見なすことができます。テスト開発者に向けた面では、ある特定のページが提供するサービスを表現します。開発者からそむけた面では、あるページ (またはページの一部) の HTML の構造について深い知識を持つただひとつのものであるべきです。Page Object 上のメソッドを、そのページの詳細や機構の露出ではなく、あるページが提供する "サービス" として考えることがもっともシンプルです。例として、任意の web ベース e メールシステム受信ボックスを考えてください。このシステムが提供するサービスの中で典型的なのは、新しい e メールを作る機能、あるひとつのメールを読む機能、そしてこの受信ボックスにある e メールのタイトル行を一覧する機能です。これらはどのようにして、テストの問題とならないように実装されるのでしょうか。

テスト開発者に、関わっているサービスについて取り組み、考えるよう促しているので、PageObjects は WebDriver インスタンスの内部をめったに露出するべきではありません。これを促進するために、PageObject のメソッドは別の PageObjects を返すべきです。これは、ユーザのアプリケーション内における旅路を効率的にモデル化できることを意味します。これはまた、お互いに関連するページを (以前にパスワードを変更していない場合に、ログインページが初めてサービスにログインしたユーザーにパスワードを変更するよう求めるなどのように) 変更する方法は、単純に適切なメソッドのシグネチャを変更することであり、これによりテストがコンパイルに失敗するべきであることも意味しています。言い換えると、ページ、およびページを反映する PageObjects の関連を変更するとき、テストを実行することなく、どのテストが失敗するかを見分けることができると言うことです。

このアプローチのひとつの結論は、(例えば) ログインの成功と失敗、またはアプリケーションの状態に依って結果が異なることがあるクリックをモデル化する必要があるかもしれないということです。このような場合、PageObject に複数のメソッドを持つことが一般的です:

public class LoginPage {
    public HomePage loginAs(String username, String password) {
        // ... ここで賢い魔法が起こる
    }
    
    public LoginPage loginAsExpectingError(String username, String password) {
        //  ... おそらくユーザ名とパスワードのどちらか、又は両方が間違っているためにログインに失敗した
    }
    
    public String getErrorMessage() {
        // こうして、適切なエラーが表示されたことを検証できる
    }
}

上記で紹介したコードは重要なポイントを示しています: PageObjects ではなく、テストは、あるページの状態に関するアサーションについて責務を負うべきであるということです。例えば:

public void testMessagesAreReadOrUnread() {
    Inbox inbox = new Inbox(driver);
    inbox.assertMessageWithSubjectIsUnread("I like cheese");
    inbox.assertMessageWithSubjectIsNotUnread("I'm not fond of tofu");
}

次のように書き直すことができるでしょう:

public void testMessagesAreReadOrUnread() {
    Inbox inbox = new Inbox(driver);
    assertTrue(inbox.isMessageWithSubjectIsUnread("I like cheese"));
    assertFalse(inbox.isMessageWithSubjectIsUnread("I'm not fond of tofu"));
}

もちろん、すべてのガイドラインと同様に例外があり、PageObjects によく見られるひとつは、PageObject をインスタンス化する際に WebDriver が正しいページ上に存在することを確認することです。これは、下記の例のように行われます。

最後に、PageObject はページのすべてを表現する必要はありません。それは、サイトナビゲーションのように、サイトまたはページ中にくり返し登場するセクションを表現するでしょう。基本原則は、PageObject がテストスイート中において特定のページ (の一部) の HTML 構造について知っているただひとつの場所であるということです。

要約

  • public メソッドは、そのページが提供するサービスを表現します
  • ページの内部を露出しないようにします
  • 一般的に、アサーションは行いません
  • メソッドは別の PageObjects を返します
  • ページのすべてを表現する必要はありません
  • 同じアクションの異なる結果は、異なるメソッドとしてモデル化されます

public class LoginPage {
    private final WebDriver driver;

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

        // 正しいページにいることを確認します。
        if (!"Login".equals(driver.getTitle())) {
            // あるいは、ログインページに誘導できるかもしれません。おそらくまずログアウトするでしょう。
            throw new IllegalStateException("This is not the login page");
        }
    }

    // ログインページは WebElements として表現される HTML 要素をいくつか含みます。
    // これら要素のロケーターはただ一度だけ定義されるべきです。
        By usernameLocator = By.id("username");
        By passwordLocator = By.id("passwd");
        By loginButtonLocator = By.id("login");

    // ログインページでは、ユーザーは username フィールドにユーザー名を入力することができます。
    public LoginPage typeUsername(String username) {
        // ここが、どのようにユーザー名を入力するか "知る" ただひとつの場所です。
        driver.findElement(usernameLocator).sendKeys(username);

        // このアクションは別の PageObject で表現されるページに遷移しないので、現在の page object を返します。
        return this;    
    }

    // ログインページでは、ユーザーは password フィールドにパスワードを入力することができます。
    public LoginPage typePassword(String password) {
        // ここが、どのようにパスワードを入力するか "知る" ただひとつの場所です。
        driver.findElement(passwordLocator).sendKeys(password);

        // このアクションは別の PageObject で表現されるページに遷移しないので、現在の page object を返します。
        return this;    
    }

    // ログインページでは、ユーザーはログインフォームを送信することができます。
    public HomePage submitLogin() {
        // ここがログインフォームを送信してホームページに遷移することが期待される、ただひとつの場所です。
        // このインスタンスに、ログインに失敗することを期待するクリックに対する別のメソッドを作成すべきです。
        driver.findElement(loginButtonLocator).submit();

        // 遷移先を表現する新しい page object を返します。ログインページが必ずどこか別の場所
        // (例えば法的免責事項) に遷移することになり、このメソッドのシグネチャを変更するということは、
        // この振る舞いに依存するすべてのテストがコンパイルできなくなることを意味します。
        return new HomePage(driver);    
    }

    // ログインページでは、ユーザーは不正なユーザー名と/またはパスワードを入力してログインフォームを送信することができます。
    public LoginPage submitLoginExpectingFailure() {
        // ここがログインフォームを送信し、ログインに失敗してログインページに遷移することが期待される、ただひとつの場所です。
        driver.findElement(loginButtonLocator).submit();

        // 遷移先を表現する新しい page object を返します。ログインに失敗する認証情報でログインフォームを送信したユーザーが
        // 必ずホームページに遷移させられる場合、LoginPage の PageObject をインスタンス化しようとするスクリプトは失敗するでしょう。
        return new LoginPage(driver);   
    }

    // 概念的に、ログインページはユーザー名とパスワードを使ってアプリケーションに "ログインする"
    // サービスをユーザーに提供します。
    public HomePage loginAs(String username, String password) {
        // ユーザー名とパスワードを入力し、ログインフォームを送信する PageObject メソッドは既に定義されており、くり返すべきではありません。
        typeUsername(username);
        typePassword(password);
        return submitLogin();
    }
}

WebDriver におけるサポート

このパターンをサポートし、同時に Page Objects からいくつかのボイラープレートコードを取り除く手助けをしてくれる PageFactory が support パッケージに含まれています。