[iOS] XCUITestをPageObjectパターンで実装してみた

## はじめに
iOSを開発し続ける中で、UITestを今までほぼ書いてこない開発人生を送ってきました。iOSDC2020で[XCUITestのつらさを乗り越えて、iOSアプリにUITestを導入する](https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&cad=rja&uact=8&ved=2ahUKEwiYlP7lwaTvAhWad94KHZU3Bk4QwqsBMAF6BAgFEAM&url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D0XGRjJBZKEY&usg=AOvVaw3tDWQLCtg49HJ5Wkphj-cs)の発表を見て、書いてみようと思い調べ実装したものをまとめました

## 開発環境
```
Xcode 12.4
Build version 12D4e
Apple Swift version 5.3.2 (swiftlang-1200.0.45 clang-1200.0.32.28)
Target: x86_64-apple-darwin19.6.0
```

## PageObjectパターンとは
- PageObjectパターンは1つのページを抽象化したオブジェクトとして扱うパターンです。
    - こちらに詳しく載ってました https://github.com/SeleniumHQ/selenium/wiki/PageObjects

## 実装してみた
発表のコードを参考にprotocolの定義します

```swift

import Foundation
import XCTest

protocol PageObjectable: AnyObject {
    associatedtype Ally
    var app: XCUIApplication { get }
    init(app: XCUIApplication)
    var exists: Bool { get }
    var pageTitle: XCUIElement { get }
    func elementsExist(_ elements: [XCUIElement], timeout: Double) -> Bool
}

extension PageObjectable {
    var app: XCUIApplication {
        return XCUIApplication()
    }
    
    var exists: Bool {
        return elementsExist([pageTitle], timeout: 5)
    }
    
    func elementsExist(_ elements: [XCUIElement], timeout: Double) -> Bool {
        for element in elements {
            if !element.waitForExistence(timeout: timeout) {
                return false
            }
        }
        
        return true
    }
}
```

### 操作したいUIに対して、`accessibility identifier`をセット
- codeでもIBでもどちらでも設定できます

![スクリーンショット 2021-03-09 8.32.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63985/0ad4cac3-f285-74ff-82aa-703377f20d35.png)

![スクリーンショット 2021-03-09 8.29.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63985/74c6c4c8-41e2-03e7-dd5d-758626c0d276.png)

### Page Objectを定義する

```swift
import Foundation
import XCTest

final class LoginPage: PageObjectable {
    let app: XCUIApplication
    init(app: XCUIApplication) {
        self.app = app
    }
    
    var pageTitle: XCUIElement {
        return app.navigationBars[Ally.title].firstMatch
    }
    
    var emailTextField: XCUIElement {
        return app.textFields[Ally.emailTextField].firstMatch
    }
    
    var passwordTextField: XCUIElement {
        return app.secureTextFields[Ally.passwordTextField].firstMatch
    }
    
    enum Ally {
        static let title = "Log in"
        static let emailTextField = "emailTextField"
        static let passwordTextField = "passwordTextField"
    }
    
    @discardableResult
    func typeEmail(_ email: String) -> Self {
        _ = emailTextField.waitForExistence(timeout: 5.0)
        emailTextField.tap()
        emailTextField.typeText(email)
        return self
    }
    
    @discardableResult
    func typePassword(_ password: String) -> Self {
        _ = passwordTextField.waitForExistence(timeout: 5.0)
        passwordTextField.tap()
        passwordTextField.typeText(password)
        return self
    }
    
    func goToSuccessPage() -> SuccessPage {
        let rightNavBarButton = app.navigationBars.children(matching: .button).firstMatch
        _ = rightNavBarButton.waitForExistence(timeout: 5.0)
        rightNavBarButton.tap()
        return SuccessPage(app: app)
    }
}
```

### UITestを書く
- あとはテストしたい処理をXCTestCaseのクラスに書きます

```swift
final class XCUITest_SampleUITests: XCTestCase {
    let app = XCUIApplication()
    override func setUp() {
        super.setUp()
        continueAfterFailure = false
    }
    
    func testLogin() {
        app.launch()
        let page = LoginPage(app: app)
            .typeEmail("test@example.com")
            .typePassword("123456789")
        
        XCTAssert(page.goToSuccessPage().exists, "successページに到達した")
    }
}
```

## はまったところ
#### 文字入力のUITestが不安定
- Simulatarの設定を修正することで改善されます。
    - Simulatarのkeyboardの設定を変更する
    - Simulatar -> I/O -> Keyboard -> Connect Hardware KeyboardをOFFにする

![スクリーンショット 2021-03-05 14.27.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63985/5dabea38-2b28-8327-98d2-42558f5aa9d4.png)

- https://stackoverflow.com/questions/55845373/disable-ios-simulator-connect-hardware-keyboard-programmatically
    - pre actionとして登録しておくのも解決策です

#### UITestの実行が不安定
- Testが壊れていないのに失敗になるケースが発生しました
- https://github.com/lyndsey-ferguson/fastlane-plugin-test_center
    - fastlaneのpluginを使うと失敗時にRetryして、なるべく失敗扱いにしないようにした。

### サンプルコード
- 実際に動くコードは下記にあります
- https://github.com/t-osawa-009/XCUITest-Sample

## 感想
- 発表を聞いた上で実装したことで理解が深まった。
- PageObjectパターンを採用することで画面に依存せずにUITestが書けるのでメンテしやすい印象を持ちました
- XcodeのUIテストが不安定な部分があるので対策が必要
- 既存のプロジェクトだといきなり全部に展開するのは難しいので小さいところから徐々に増やしていきたいです

## 参考
- https://github.com/SeleniumHQ/selenium/wiki/PageObjects
- http://yfj2.hateblo.jp/entry/2014/11/02/103524
- https://speakerdeck.com/satotakeshi/xcuitestfalseturasawocheng-riyue-ete-iosahuriniuitestwodao-ru-suru