こんにちは。iOS エンジニアの永田です。今日は XCUITest で UI テストを書いたことがある方向けに、UI テストをより分かりやすく書くための Page Object を紹介したいと思います。
Page Object とは
Page Object とは、アプリケーションのページを抽象化したオブジェクトのことです。Page Object はそのページ固有の操作やアサーションに必要なページの状態を提供します。
本記事では、Page Object を利用するモチベーションとその実装例をご紹介します。
UI 統合テストでの問題点
XCUITest を利用すると、UI の要素やその状態の取得、タップなどの操作をメソッドを呼ぶだけで簡単に実現することができます。しかし、テストケースごとに必要な要素を用意してシナリオを満たすようにコードを書いていくと、1つのテストケースが非常に長いコードになってしまうことがあります。たとえば、以下のような新規登録登録フローについて考えてみます。
このフローをそのままコードに落としていくと、以下のようなものになるでしょう。
func testSignup() {
let app = XCUIApplication()
// Start from welcome page
app.buttons["Singup"].tap()
// Move to sign up page
let emailField = app.textFields["emailField"]
let passwordField = app.secureTextFields["passwordField"]
let nameField = app.textFields["nameField"]
XCTAssert(emailField.exists)
XCTAssert(passwordField.exists)
XCTAssert(nameField.exists)
// Fill email, password and name fields
emailField.typeText("smith@example.com")
passwordField.typeText("jg28hwm90iflz")
nameField.typeText("Smith")
// Tap "create an account" button
app.buttons["createAccount"].tap()
// Move to complete page
let heading = app.staticTexts["heading"]
XCTAssert(heading.exists)
XCTAssertEqual(heading.label, "Welcome, Smith!")
// Select country
app.pickers["CountryPicker"]
.pickerWheels
.element(boundBy: 0)
.adjust(toPickerWheelValue: "Japan")
// ...
}
さて、課題は何でしょうか。
一つは、「テストケースごとに同じようなコードが書かれる可能性がある」ということです。
もう一つは、「アプリケーションの操作や状態取得のコードが意図がわかりづらい」ということです。あるテストケースで開発者が知りたいことは、ページに対する一連の操作と状態のアサーションだけのはずです。また、上のようなコードコメントを書きたいと感じるのは抽象化が不十分なサインです。
これらの問題を踏まえると、UIをある程度の塊で抽象化をはかり再利用可能な状態にするのが良いと考えます。そこで「ページ」という概念を考えます。「スクリーン」や「画面」と言っても良いでしょう。この「ページ」を一つのクラスとしてまとめ、「操作」や「状態」をメソッドやプロパティとしてまとめるとどうでしょう。他のシナリオで再利用可能で、テストケース内でも短く簡潔に書けるのではないでしょうか。
Page Object を使ったテストコード
Page Object を利用すると以下のようなコードになります。どのような操作がされて、どのようなページに遷移して、どのような状態が期待されているかがはっきりします。
func testSignup() {
let app = XCUIApplication()
let signUpPage = WelcomePage(app: app)
.tapSignUpButton()
let completePage = signUpPage.
.typeEmail("smith@example.com")
.typePassword("jg28hwm90iflz")
.typeName("Smith")
.tapCreateAccountButton()
_ = completePage
.then { XCTAssertEqual($0.headingText, "Welcome, Smith!") } // It is like devxoul/Then
.pickCompany("Japan")
// ...
}
実装について
さて、Page Object の概要とモチベーションを説明してきました。ここからはどのように実装していくかについてお話しします。まず、 Paeg protocolを用意します。Page protocol は app と view の2つのプロパティを持ちます。 app はPageのコンテキストとして持ち、viewの定義と他の Page を作成する際にのみ利用します。view は Page 全体を表す UI 要素で、例えば、ある View Controller の view などがそれに当てはまることが多いでしょう。 init では app を受け取り、self の app を初期化するのと同時に view が存在することをアサートします。
protocol Page {
var app: XCUIApplication { get }
var view: XCUIElement { get }
init(app: XCUIApplication)
}
まずは「ウェルカムページ」の Page protocol を満たす最低限の実装を見ていきましょう。waitForElementToAppear は、要素が存在することを確認するためのアサーションメソッドだと思ってください。
class WelcomePage: Page {
let app: XCUIApplication
var view: XCUIElement { return app.otherElements["WelcomeViewController"] }
required init(app: XCUIApplication) {
self.app = app
waitForElementToAppear(view)
}
}
続いて、操作を書いていきましょう。ウェルカムページには、ログインボタンとサインアップボタンがあり、どちらかを押すことによって次のページが開くとします。それぞれの操作を以下のように書きます。
class WelcomePage: Page {
// ...
private var loginButton: XCUIElement { return view.buttons["loginButton"] }
private var signUpButton: XCUIElement { return view.buttons["signUpButton"] }
func tapLoginButton() -> LoginPage {
loginButton.tap()
return LoginPage(app: app)
}
func tapSignUpButton() -> SignUpPage {
signUpButton.tap()
return SignUpPage(app: app)
}
}
ポイントは以下です。
- 各要素は computed property で定義することで各操作で再利用可能にし、かつ参照時に要素を探すようにすること
- 各要素は原則 view から参照すること (appではなく)
- 各メソッドは、遷移が発生する場合、遷移先のページを返すこと (そうでない場合はSelfを返すこと)
次に、ページの状態をアサートしたいケースについても確認してみましょう。例えば、サインアップページで、パスワードがポリシーに満たしているかどうかバリデーションし、エラーが表示されるというケースを想定します。こう言ったケースでは現在の状態を返す computed property を追加します。
class SignUpPage: Page {
// ...
var errorMessage: String {
return view.staticTexts["errorLabel"].label
}
}
この property を利用してアサーションします。
signUpPage
.typeEmail("")
.typePassword("3i&wu,iu87n")
.then { XCTAssertEqual(signUpPage.errorMessage, "Email can't be blank.") }
まとめ
最後に簡単にまとめます。
- Page という単位で抽象化すると上手くいく
- Page は操作や状態を抽象化する
- Page は初期化時に存在をアサートする
- 操作メソッドは操作後の Page を返す
自分は長らくUIテストや統合テストの煩雑さに悩まされてきましたが、Page Object を導入したことによって読みやすくDRYなコードを書くことができるようになりました。導入は非常に簡単なのでぜひご活用ください。
参考:
- Martin FowlerによるPage Objectの紹介 https://martinfowler.com/bliki/PageObject.html
- SeleniumによるPage Objectの紹介 https://www.seleniumhq.org/docs/06_test_design_considerations.jsp#page-object-design-pattern
- Page Objectの実装例 https://blog.getgauge.io/are-page-objects-anti-pattern-21b6e337880f