Java
test
appium
page-object

Appium java-clientでPage Objectsパターン

Appiumは言わずと知れたモバイルアプリケーション用のOSSテスト自動化ツールです。
今回はそのAppiumでPageObjectパターンを書く方法をまとめたいと思います。

PageObjectsパターン

みなさんはPageObjectsパターンをご存知でしょうか?

SeleniumHQのPageObjectsのページによれば

  • 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

と書いてあります。
私の理解を含めて訳すと、

  • publicなメソッドはそのページが提供する論理的な処理を表す
  • ページ内部の情報は公開しない
  • テストで使用するアサーションはPageObjects内に含まない
  • メソッドはPageObjectsを返す
  • ページ全体を表す必要はない
  • 同じアクションでも結果が異なる場合は異なるメソッドとして定義する

つまり、PageObjectsは(テストとは関係なく)ページとユーザの関係を表したオブジェクト、というのが私の理解です。

メリットなど詳しく知りたい方は以下を購入して読んでみるとよいと思います。
Seleniumデザインパターン & ベストプラクティス

Appium java-clientでのPageObjectsをサポートする機能

https://github.com/appium/java-client/blob/master/docs/Page-objects.md
に記載されている機能がPageObjectsを記述する上で非常に協力なサポートになります。

ここでは簡単にまとめたいと思います。

アノテーションベースでのElement指定

例えば、AndroidのElementを指定する場合、以下のように指定することで取得できます。(someStrategy部分にはidやxpathなどのElementの指定方法が入ります。以降も同じです)
非常に簡単ですね!!

@AndroidFindBy(someStrategy) 
AndroidElement someElement;

CrossPlatform対応

AppiumはiOS,Android,WebやWebViewなどもサポートしています。
Elementの指定方法だけ変えてCrossPlatform対応ができたら便利ですよね!?

なんとそんな便利なことができますよ!

例えば、iOS,Androidを指定する場合はこのようになります。

@AndroidFindBy(someStrategy) 
@iOSFindBy(someStrategy) 
MobileElement someElement;

iOS,Androidそれぞれのアノテーションをつけ、両OSで対応できるクラスのMobileElementを指定します。

両OSに加えてWebも含める場合はどうするでしょう?

@FindBy(someStrategy) //for browser or web view html UI
@AndroidFindBy(someStrategy) //for Android native UI 
@iOSFindBy(someStrategy)  //for iOS native UI 
RemoteWebElement someElement;

このように両OSとWeb用のアノテーションをつけ、対応するRemoteWebElementを指定します。

OSとクラスの関係は以下のようになります。

OS クラス
iOS IOSElement
Android AndroidElement
Web WebElement
iOS,Android MobileElement
iOS,Android,Web RemoteWebElement

Chain,All possible

さて、これでElementを指定できるようになりましたが、
特定のElementの中のElementを指定する場合はどうするでしょうか?
また指定したいElementが異なる指定方法で複数ある場合はどうするでしょうか?
複雑になってもxpathで指定するのでしょうか?

これについて解決方法が提供されています。

@HowToUseLocators(androidAutomation = CHAIN, iOSAutomation = ALL_POSSIBLE)
@AndroidFindBy(someStrategy1) @AndroidFindBy(someStrategy2) 
@iOSFindBy(someStrategy1) @iOSFindBy(someStrategy2) 
RemoteWebElement someElement;

だいたい雰囲気で分かるでしょうが、CHAINで指定するとAND条件(正確には、someStrategy1で指定したElement配下のsomeStrategy2で指定したElement)になり、ALL_POSSIBLEであればOR条件になります。

デフォルトはCHAINなので、CHAINのみで指定する場合は@HowToUseLocatorsを書かなくても問題ありません。

Widget

実際の画面を見ていると、いくつかの画面に共通で存在するものがありますよね?
例えばヘッダーなど共通化して使いたいということがあると思います。

その場合にはWidgetを継承したクラスを定義します。

@AndroidFindBy(someStrategy)
public class Header extends Widget {

    public Header(WebElement element) {
        super(element);
    }

    @AndroidFindBy(someStrategy)
    private MobileElement back;
}

コンストラクタを定義して、親コンストラクタを呼び出す必要があります。
Elementの指定方法は通常と変わりません。
クラスにアノテーションがついてるのは、共通化している部分を指定しています。
画面ごとに共通化している部分の指定方法が異なる場合は、PageObjectsに変数のWidgetを定義するときにアノテーション指定をします。

PageObjectsの生成

さて、いままでアノテーションとWidgetについて見てきましたが、これから実際のPageObjectsをどのように定義するかを見ていきます。

まずはプレーンなPageObjectsを見てみましょう。

public class SamplePageObject {

    public Header header;

    @AndroidFindBy(someStrategy)
    private MobileElement hogehoge;

}

実はこのクラスを生成しただけでは、Elementはnullのままとなってします。
実際のElementを代入するためには、PageObjectsを生成したあとに以下のような処理を記述する必要があります。

SamplePageObject pageObject = new SamplePageObject();
PageFactory.initElements(new AppiumFieldDecorator(webdriver, 
              pageObject //an instance of PageObject.class
);

これでElementsに実際の画面のElementが代入されます。

PageObjectsパターンの実装方法

実はいままで紹介してきたのはappium java-clientのwikiに記載されている内容であり、そのwikiには実際のPageObjectsパターンを実現する方法が記述されていません。

なので、これからは私が考えた、PageObjectsパターンを最大限活かした実装方法を紹介したいと思います。

ベースPageObjects

まずPageObjectsが生成された時点でElementが指定されるように、ベースPageObjectsクラスを作ります。

public abstract class PageObject {

    protected WebDriver driver;

    public PageObject(WebDriver driver) {
        this.driver = driver;
        PageFactory.initElements(new AppiumFieldDecorator(this.driver), this);
    }

    // 必要であれば共通処理
}

このクラスを継承したクラスはWebDriverを引数に親コンストラクタを呼び出すだけでElementに代入されるようになります。

public class TopPage extends PageObject {

    public TopPage(WebDriver driver) {
        super(driver);
    }

    // Elementの指定
}

publicメソッドの実装

最初に書いたように

publicなメソッドはそのページが提供する論理的な処理

を表します。

実際の画面をみてみると、多くの処理は画面遷移を伴います。
なので、メソッドは次の画面を返すように実装するとよいと思います。

public class TopPage extends PageObject {

    public TopPage(WebDriver driver) {
        super(driver);
    }

    @AndroidFindBy(id = "jp.co.android.sample:id/hogehoge")
    private MobileElement hogehoge;

    @AndroidFindBy(id = "jp.co.android.sample:id/refresh_button")
    private MobileElement refresh;

    public HogeHogePage moveToHogeHoge() {
        hogehoge.click();
        return new HogeHogePage(this.driver);
    }    

    public TopPage refresh() {
        refresh.click();
        return this;
    }    
}

WebDriverはこのようなときに必要になるため、親クラスのコンストラクタで保存してました。
また画面が遷移しないときは、自身を返すとよいでしょう。

画面遷移の検証

これはGebに影響を受けてなのですが、画面遷移後にその画面に正しく遷移できているか、確認するメソッドを定義しておいたほうがよいと考えています。

親クラスのコンストラクタで検証をしてしまいます。

public abstract class PageObject {

    protected WebDriver driver;

    public PageObject(WebDriver driver) {
        this.driver = driver;
        PageFactory.initElements(new AppiumFieldDecorator(this.driver), this);

        if(!at()) {
            throw new RuntimeException("指定した画面遷移ができませんでした。:" + this.getClass());
        }
    }

    protected abstract boolean at();
}

画面遷移が失敗したらエラーにしていて強引だと感じるかもしれませんが、
正しく画面遷移できていない場合、後続の処理でエラーになってしまいます。
そのときのエラーメッセージに比べれば、こちらのほうが親切だと思います。

このatメソッドは子クラスで実装します。
普通はその画面の固定の要素(例えばタイトルだとか)があるはずなので、その存在の有無やテキストの比較を行えばよいでしょう。
なければ、最悪常にtrueを返すでも問題ないとは思います。

検証方法について

みなさん、PageObjectsパターンのときに検証はどうしていますか?
例えば、画面のあるElementが表示されているかどうか、を確かめたいときにどうしますか?

私はPageObjects側にそのようなメソッドは持たせたくないです。
それは、最初に言った通り、PageObjectsは(テストとは関係なく)ページとユーザの関係を表したオブジェクト、というのが私の理解だからです。
(ユーザは検証はしないですよね?)

じゃぁどうするか?
これには正解がない、と思っています。

Elementをpublicなフィールドとして定義して検証に使用する、というもの1つの解決方法だと思います。
ただ、気をつけなければならないことは、このissuesに書いてあるように、WidgetのElementはpublicメソッドが呼ばれたときに代入されるため、それ以外ではnullになってしまいます。

私の中での今のところの回答は、検証するためのメソッドは極力最小にPageObjectsに定義する、です。

まずどうしてこのような結論になったか、というと

  • 上記に書いたように、WidgetのElementはpublicメソッドが呼ばれたときに代入されるため、それ以外ではnullになってしまうため、Elementをpublicなフィールドにしたくない
  • 画面の確認に検証に用いる大部分は、ある項目が表示されているかどうかだと思う
  • 検証用のメソッドが多すぎると、ページとユーザの関係を表している、という思想からずれてしまう

という理由からです。

では、それをどのように実現するか、というと、私の場合は親クラスに検証用のメソッドを生成しました。

public abstract class PageObject {

    protected WebDriver driver;

    public PageObject(WebDriver driver) {
        this.driver = driver;
        PageFactory.initElements(new AppiumFieldDecorator(this.driver), this);
    }

    public boolean isDisplayed(String fieldName) {
        Class aClass = this.getClass();
        try {
            Field declaredField = null;
            while (aClass != null) {
                try {
                    declaredField = aClass.getDeclaredField(fieldName);
                    break;
                } catch (NoSuchFieldException e) {
                    aClass = aClass.getSuperclass();
                }
            }
            if (declaredField == null) {
                throw new NoSuchFieldException();
            }

            declaredField.setAccessible(true);
            Object field = declaredField.get(this);

            if (field instanceof MobileElement) {
                MobileElement element = (MobileElement) field;
                return element != null && element.isDisplayed();
            }

            throw new NoSuchFieldException("指定したFieldがMobileElementとして定義されていません。:" + fieldName);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            throw new RuntimeException("指定したFieldは定義されていません。:" + fieldName);
        }
    }
}

この方法のメリットは、上記の3点の理由をカバーしている点です。
逆にデメリットは

  • どこまで汎用的に使えるか分からない
  • PageObjectsのフィールドを知らないといけない

という点です。

ですが、その1つ目のデメリットについて本当に個人的な意見ですが、検証項目を増やしすぎると保守コストが高くなるため、検証自体は極力しなくてよい(前述のatメソッドで十分)、と思っています。
とはいえ、画面の要素が表示されているかどうかを検証することは必ずあると思ったので定義しています。

最後に

Appium自体は有名になったいてよく耳にしますが、
Appium java-clientに関する日本語の記事が少なくて自身は苦労しました。
ぜひみなさんがどうやってjava-clientを使っているかを情報共有してもらえると嬉しいです。

またAppium自体どのような言語が主流なのかわかっていないです。
どの言語に優位性があるかなどあれば教えてもらえると助かります。