[iOS] Xcode9のUI TestでSafariとか設定を操作して、自分のアプリをテストする

Unit TestよりもUI Testでテストした方が良いことがあります。

Xcode9から`XCUIApplication`が強化され、よりUI Testの幅が広がりました。
今回はその中の他のアプリの操作について書きたいと思います。

他のアプリを操作することによって、URLスキームのテストとか、カメラや写真、通知など各パーミッションのテストが可能になります。

- SafariでURLを入力してスキームのテスト
- iOSの設定を操作して、各パーミッションの許可/不許可を変更する
- TIPS
  - 操作対象エレメントの取得方法がわからない場合
  - 言語設定
  - 基本古いXcodeでビルドしている場合
  - Apple defaultアプリのBundle IDがわからない

# SafariでURLを入力してスキームのテスト
スキーム起動に対応していていて画面やその他の状態によりスキーム実行された時の解釈が異なる場合に全てのケースを網羅的にテストしておきたいことがあります。

Xcode8でも出来なくもなかったのですが、やや複雑で正攻法ではなかったためその分シンプルではありませんでしたし、メンテナンス性に欠ける記述になってしまうのであまりよくありませんでした。
Xcode9から`XCUIApplication`をBundle IDからイニシャライズできるようになりました。つまり自分のアプリ以外のアプリを操作できるようになりました。

具体的にはSafariでURL(スキーム)を入力して自分のアプリを呼びたい場合、下記のように書けます。

```swift
let safari = XCUIApplication(bundleIdentifier: "com.apple.mobilesafari")
// Safari起動
safari.activate()
if safari.otherElements["URL"].exists {
    safari.otherElements["URL"].tap()
}

// URL入力
let addressBar = safari.textFields["URL"]
addressBar.typeText(url)
safari.buttons["Go"].tap()

// アプリスキーム起動
safari.buttons["Open"].tap()

// アプリの復帰を待つ
let app = XCUIApplication()
XCTAssertTrue(app.wait(for: .runningForeground, timeout: 30))
```

意図通りの画面が表示されているかは以下のような形で`XCTAssert`を使用して検証することができます。

```swift
XCTAssert(app.navigationBars["意図している画面タイトル"].exists)
```

![uitest_open_safari_short.mov.gif](https://qiita-image-store.s3.amazonaws.com/0/156085/ef497989-2bbe-5327-11f2-a9ccae855d17.gif)


# iOSの設定を操作して、各パーミッションの許可/不許可を変更する
写真へのアクセスや通知の許可状況を変更してアプリの挙動をテストしたい場合があります。
この場合もSafariと同様にBundle IDから`XCUIApplication`を生成、iOSの設定を操作することで可能になります。

```swift
// 設定アプリを取得
let settings = XCUIApplication(bundleIdentifier: "com.apple.Preferences")

// 起動
switch settings.state {
case .notRunning:
    settings.launch()
case .unknown, .runningBackgroundSuspended, .runningBackground:
    settings.activate()
    // ルート階層に戻す
    var backButton: XCUIElement
    repeat {
        backButton = settings.navigationBars.buttons.element(boundBy: 0)
        if backButton.exists {
            backButton.tap()
        }
    } while backButton.exists
case .runningForeground:
    // Nothing to do
    break
}

// ルート階層から辿る
let tablesQuery = settings.tables
tablesQuery.staticTexts["Privacy"].tap()
tablesQuery.staticTexts["Photos"].tap()
if tablesQuery.staticTexts["自分のアプリ名"].exists {
    tablesQuery.staticTexts["自分のアプリ名"].tap()

    if authorized {
        // 許可する
        tablesQuery.staticTexts["Read and Write"].tap()
    } else {
        // 不許可にする
        tablesQuery.staticTexts["Never"].tap()
    }
}

// 自分のアプリを起動する
let app = XCUIApplication()
app.launch()
```

![uitest_open_settings_short.mov.gif](https://qiita-image-store.s3.amazonaws.com/0/156085/10a06aaf-3b84-4ad3-0d4a-b5016d1df1f1.gif)

# TIPS
## 操作対象エレメントの取得方法がわからない場合
UI Testでは実際の操作をそのままコード化できる仕組みが用意されています。
以前のXcodeではJavaScriptのコードに落とし込むような感じだったと思いますが進化し、確かにこちらの方が直接的ですね。

1. recordingボタンを押す
  ![uitest_recording.png](https://qiita-image-store.s3.amazonaws.com/0/156085/53828453-8458-ecf6-7838-626dcf90495e.png)

2. 操作する
3. 停止する（もう一度recordingボタンを押下）

そのままだと異常にネストが深いエレメント取得になっている場合があるので、
よりシンプルでわかりやすいコードに変える必要があります。

UI Test実行して、ブレークポイントを貼れば停止するので、その時点で何がどのような形で取得できるのか、Debug Console上で調べるとより簡潔に取得できるコードがわかります。



## 言語設定
言語設定が英語ではない場合、サンプルで示したコードではうまく通りません。`tablesQuery.staticTexts["Privacy"].tap()`などで取得できずにfail扱いになります。
下記のように分岐しておけば、日本語環境でも実行できますが、もっとスマートな方法がないものか模索中です。

```swift
if tablesQuery.staticTexts["Privacy"].exists {
    tablesQuery.staticTexts["Privacy"].tap()
} else if tablesQuery.staticTexts["プライバシー"].exists {
    tablesQuery.staticTexts["プライバシー"].tap()
} else {
    XCTFail()
}
```

## 基本古いXcodeでビルドしている場合
テストコードに以下のような分岐を設けておけば、Xcode8以前の環境では実行されないため、エラーになりません。

```swift
#if swift(>=3.2)
    // Xcode9以降でのみ実行したいコード
#endif
```

## Apple defaultアプリのBundle IDがわからない
ググると以下のようなサイトが出てきます。
命名規則が結構ガバガバなんですね:sweat_smile:

[iOS 10 List of default apps and bundle ID’s](https://emm.how/t/ios-10-list-of-default-apps-and-bundle-ids/361)