この記事は ビビッドガーデン Advent Calendar 2021 の 9 日目です。
こんにちは!
ビビッドガーデンにてiOSアプリ開発をさせていただいております、田中です!
iOSのアプリケーションにUIテスト(E2Eテスト)を組み込む際、アーキテクチャとしてPageObjectパターンを適用したので、実際に適用してみた感想をお伝えできればと思います。
PageObjectパターンの実装方法などは既にドキュメントも多々存在しますので、今日はあくまで感想を伝えさせて頂き、UIテストアーキテクチャ選定の参考になれば嬉しいです🙌
PageObjectパターンって?
感想をお伝えする前に、PageObjectパターンについて簡略化して説明させていただきます。
PageObjectパターンとはテストに必要なコードの中から UIに関するコードをPageObjectというクラスに委譲する アーキテクチャです。
例えば、アプリのホーム画面からマイページに遷移するテストをしたい場合、UIテストコードに必要な項目は下記コメントのようになります。
func myPageSceneTest() {
// 1. テストアプリケーションの起動
// 2. ホーム画面の表示担保
// 3. マイページボタン(場合によってはタブなど)の存在確認
// 4. マイページボタンのタップ
// 5. マイページが表示されたか要素からAssertテスト
}
上記のコードの内、1〜4 は UI操作 であって テストコード ではありません。
なので、テストと関係ないUI操作の部分をPageObjectに委譲させて下記のようにするのが、本アーキテクチャの考え方です。
※あくまで例なので色々省略してます🙇♂️
// ↓これがHomeのUIを担当するPageObjectクラス
class Home {
init() {
// 1. テストアプリケーションの起動
}
// マイページボタンのタップ関数
func tapMyPageButton() -> MyPage {
// 2. ホーム画面の表示担保
// 3. マイページボタン(場合によってはタブなど)の存在確認
// 4. マイページボタンのタップ
return MyPage()
}
}
// ↓これがMyPageのUIを担当するPageObjectクラス
class MyPage {
var hasHoge: Bool {
// ex. hoge.exists
}
}
// ↓これがテストコード
func myPageSceneTest() {
let myPage = Home().tapMyPageButton()
// 5. マイページが表示されたか要素からAssertテスト
// ex. XCTAssertTrue(myPage.hasHoge)
}
実際にiOSアプリのUIテスト(E2Eテスト)に適用してみた感想
実際に適用して運営してみたところ、下記のようなメリットが見えてきたのでお伝えさせていただきます。
- 汎用性が高くて楽そう
- ViewController中心のアーキテクチャとの相性は良さそう
- waitForExistence関数との相性がGood
- 静的な定義をフレームワークで共有している場合、AccessibilityIdentifierの管理がしやすい
- QAエンジニアとの作業分離がしやすそう
【おまけ】最後に…気になるところとデメリット
汎用性が高くて楽そう
これはとても大きなメリットかと思います。
ここでいう汎用性には2つの側面があり、それぞれ下記の通りです。
UI操作に関するコードをコピペしやすい
UIの操作というものはそこまで沢山種類があるわけではなく、ボタンタップ
Cell選択
スワイプ
などある程度アクションは決まっています。
なので、例えば Homeのマイページボタンタップ
に関するUI操作のコードが作成済みであれば、Homeのお気に入りボタンタップ
などは、対象となる要素の指定部分を変更するだけで十分対応できる可能性が高いわけです。
すでに作成済みのUI操作をそのまま使いまわせる
例えばHomeのPageObjectでマイページボタンをタップする関数を既に作成済みであれば、以後追加されるテストで マイページに遷移させたい という要望があるときに、その関数を呼び出すだけで済みます。
つまり、PageObjectに作成したUI操作は、以後のテストでも十分活用できるシーンがあるということです!
ViewController中心のアーキテクチャとの相性は良さそう
昨今、SwiftUIを採用するアプリも多くなってきたと思いますが、未だViewControllerを軸に据えたアプリも多く存在すると思います。
そういったアプリの場合、存在するページ分だけViewControllerがあると考えられます。
つまり 各ViewController対応したPageObjectを作ればUI操作が網羅される というようなイメージで構築できます。
なので**「待てよ?この画面ってどのPageObject??」**といった疑問が浮かぶことは少なく、共通認識を得やすい構成を作れると思います。
waitForExistence関数との相性がGood
iOSのUIテストで要素を捕捉すると、XCUIElementというクラスで表現されます。
このXCUIElementには waitForExistence という関数が用意されていますが、この機構がすごくPageObjectパターンと相性が良いと感じました。
waitForExistence は引数にインターバルを指定することで、存在が確認されるまで指定時間だけ要素を待ち受けてくれます。
これを利用して、下記のように存在確認まで担保したPageObjectの関数を作ることができます。
// ↓HomeのUIを担当するPageObjectクラス
class Home {
// マイページボタンのタップ関数
func tapMyPageButton() throws -> MyPage {
// waitForExistence で描画が完了してボタンが存在するまで処理を待たせる
guard myPageButtonElement.waitForExistence(3) else {
// UIが整わなかったら「UIのエラーだよー!」と分かるエラーをthrowする
throw UIError
}
// ちゃんと描画されたか担保できた時点でタップを実行
myPageButtonElement.tap()
return MyPage()
}
}
上記のようにUI操作を作成することで、UIに関するエラーだった時の補足も分かりやすくできます。
また 描画タイミングによってテスト結果が違う などという不測の事態を防ぐようなUI操作を定義できます。
静的な定義をフレームワークで共有している場合、AccessibilityIdentifierの管理がしやすい
iOSのUIテストで要素を補足するにはアプリ側でUIパーツに対して定義された AccessibilityIdentifier というプロパティを参照する必要があります。
これはHTMLでいうところの id を定義&参照するようなイメージです。
昨今ではアプリ内のコードをレイヤーや役割別にフレームワーク化して管理しているプロジェクトも多いかと思います。
そういったプロジェクトの場合、全体で共有するようなレイヤーのフレームワークに AccessibilityIdentifier の定義ファイルを置くことで、UIへの定義とPageObjectからの呼び出しの両方で同一のファイルを参照できます。
こうすることで、タイポによる要素の取得エラーやIdentifierの重複回避などが出来るので安心です。
// ↓アプリでのUIパーツ定義
class MyPageButton: UIButton {
func setup() {
accessibilityIdentifier = AccessibilityIdentifierConstants.homeMyPageButton
}
}
// ↓HomeのPageObjectからのボタン要素取得
class Home {
private var myPageButton: XCUIElement {
return app.buttons[AccessibilityIdentifierConstants.homeMyPageButton].firstMatch
}
}
// ↓共通フレームワーク内での静的AccessibilityIdentifier文字列定義
public struct AccessibilityIdentifierConstants {
public static let homeMyPageButton = "home_mypage_button"
}
QAエンジニアとの作業分離がしやすそう
iOSのUIテスト(E2Eテスト)にQAエンジニアが参画する上で、一番の障壁になるのはiOS独特のライフサイクルやUI要素を理解しなければいけない点だと思われます。
また、QAエンジニアがアプリの実装に関わっていない場合、そもそも要素がどのようなUIクラスなのかも把握できていなかったり、遷移に必要なUI操作手順が明確でないことなどもあると思います。
ですがPageObjectパターンで実装されていれば、QAエンジニアが担当するのはテストコードだけ というように割り切ることが可能そうに思いました。
例えば下記のように、QAエンジニアがテストケースを作成し、遷移ロジックはアプリエンジニアが担当する
といった進め方も実現しやすいと思います。
// テストケースはQAエンジニアが作成
func myPageTransitionTest() {
XCTContext.runActivity(named: "マイページへの遷移テスト") { _ in
// UI操作はアプリエンジニアに丸投げする
// ex.
// to アプリエンジニア
// マイページへの遷移後、マイページのタイトル要素をmyPageTitleに格納処理をお願いします!
XCTAssertTrue(myPageTitle.exists)
}
}
最後に…気になるところとデメリット
SwiftUIとの相性
今回導入した「食べチョク」アプリではSwiftUIへの移行を検討している段階です。
なのでSwiftUIを選択したプロジェクトでPageObjectパターンがちゃんとハマるかどうかは未実証になります。
今後SwiftUIへの移行とともに、改めてご報告できたらと思います!
チェーンメソッドにすると冗長になりやすい
PageObjectパターンでは前述したコード例にもあるように、チェーンメソッドを利用することが多いようです。
これは一つ一つの操作毎にチェーンさせることで、可読性の向上、汎用性の向上、次のPageObjectへの橋渡しが容易になるためだと思われます。
ですが、同時に複雑な遷移になると冗長になりやすいというデメリットもあります。
こうした場合、一連の処理をまとめた関数を用意するなど、対応が必要になってくることもありそうでした。
テストの為のテストがPageObjectに含まれがち
例えば、Homeからマイページに遷移させるPageObjectのロジックを作成する際に、まずHomeが表示されていることを担保するコードも必要になります。
このようにPageObjectの中にはテストのために必要なテストが含まれることが多くなります。
場合によっては遷移ロジックより、こうした要素チェックのコードの方が多くなる場合もありそうなので、工夫が必要そうでした。
以上、実際にPageObjectパターンをiOSのUIテスト(E2Eテスト)に適用して感じたことまとめでした!
今後また何か気づき次第、共有できたらと思います!
株式会社ビビッドガーデンでは、iOSエンジニアを募集しております🙌