XCUITest の最初のテスト

dwango2 Advent Calendar の記事です。四日目は iOS の話です。
こんにちは、 @daichiro です。
ドワンゴには **iOS 四天王**というのがおり[要出典]、私もその一人でしたが、四天王と揶揄されるのが恥ずかしいので今はその地位を譲りました。
若いエンジニアでも活躍できる、そして四天王が作られるほど iOS エンジニアがいる良い環境であると言えます。

さて、みなさん UITest は書いているでしょうか。
アプリの振る舞いをプログラムで制御し、画面の要素や画面繊維が期待した通りになっているかを調べることができます。
利用されるシーンとしては、アプリのバージョンアップ前に既存機能が壊れていないか調べる **リグレッションテスト** が多いかと思います。

リグレッションテストは多くのプロジェクトで行われていると信じていますが、それが自動化されているかというと、数えるほどになってしまうという感覚です。

今回は簡単なサンプルアプリに対して UITest をサクッと書くという記事です。
皆さんのプロジェクトの導入のきっかけになれば幸いです。

# 導入

新しく Xcode Project を作成する際は、 Test を作るチェックマークを入れておけば自動的に作られます。
既存のプロジェクトに追加する場合はいくつか手順が必要です。
これは Qiita に様々な記事があるので参照されると良いと思います。

ここまでできたと仮定して、テストを書いてみましょう。

# 最初のテスト

UITest で最も初歩的なのは、表示を確認するものだと思います。

- 画面の要素を確認する
- 画面遷移を確認する

ログインしているかどうか、プレミアム会員かどうか、というお客様の状態によって画面の要素が変化するというのはよくあるので、画面にある要素の確認のテストは必要ですね。
しかし画面の要素に対して何か操作を行うものの方が UITest をやっている感があるでしょうか。
ということで、画面遷移のテストを書いてみましょう。

### コード

```swift
import SwiftUI

struct ContentView: View {
    @State private var toDetailView = false
    var body: some View {
        NavigationView {
            VStack {
                Button("Go to detail") {
                    toDetailView = true
                }
                NavigationLink(
                    destination: DetailView(),
                    isActive: $toDetailView,
                    label: { EmptyView() })
            }
            .navigationBarTitle("Content")
        }
    }
}

struct DetailView: View {
    var body: some View {
        VStack {
            Text("This is detail view")
        }
        .navigationBarTitle("Detail")
    }
}
```

`ContentView` の中心に存在する `Go to detail` と書かれたボタンをタップすると、`DetailView` に遷移するはずです。

### テスト

```swift
import XCTest

final class ContentViewUITests: XCTestCase {
    func testMoveToDetailView() {
        let app = XCUIApplication()
        XCTContext.runActivity(named: "Launch app") { _ in
            app.launch()
        }
        XCTContext.runActivity(named: "Check content view") { _ in
            XCTAssertTrue(app.navigationBars["Content"].exists)
        }
        XCTContext.runActivity(named: "Tap button and show detail view") { _ in
            app.buttons["Go to detail"].firstMatch.tap()
            XCTAssertTrue(app.navigationBars["Detail"].exists)
            XCTAssertTrue(app.staticTexts["This is detail view"].exists)
        }
    }
}
```

これを実行すると、シミュレータが起動し、テストが実行されます。

![sample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/56442/5f5a1c4c-c4b6-9d5d-88d4-d54f58e067a1.gif)

アプリが起動して画面遷移を一瞬のうちに行い、テストは成功です。

#### XCUIApplication

```swift
let app = XCUIApplication()
app.launch()
```

XCUITest においては `XCUIApplication` を中心としてコトが進んでいきます。
これは起動するアプリそのものと言っていいでしょう。

`navigationBars` `buttons` `staticTexts` `images` などのプロパティが生えていますが、これは **現在表示されているスクリーン** に対してアクセスできるものが取得できます。
これらは `XCUIElementQuery` という型になっていて、Label や Identifier を使って特定の要素にアクセスできます。

例えば、Content View が開かれている時には `app.navigationBars["Content"]` は一件ヒットします。
逆に言えば、Detail View が開かれている時にはヒットしないので、現在の画面が何であるか、要素が存在するかということのチェックに使えるというわけです。

より詳しく知りたい場合は公式ドキュメントを探ってみてください。

## 変更に強くする

さて、先ほどのテストで今はうまくいっていますが、そのうち動かなくなることは明白です。
なぜなら、要素に直にアクセスしているからです。

```swift
app.buttons["Go to detail"].firstMatch.tap()
```

この文を例にとると、`Go to detail` という文字列を直に検索しているので、ここが何かの変更によって書き換えられるとたちまちテストは通らなくなります。
文字列自体に意味があれば別ですが、ここでは Detail View に遷移するボタンさえあれば、ボタンの様子は気にしません。
また、他に `Go to detail` と書かれたボタンが置かれたら想定しないボタンをタップしてしまうこともあります。

ではどうすれば良いかというと、 **AccessibilityIdentifier** を使います。

### AccessibilityIdentifier

アクセシビリティ？多言語対応？ボイスオーバー？などと思うかもしれませんが、**別物です**。
ユーザーに見える `accessibilityLabel` とは違って、こちらは開発者向けのプロパティです。(多分)

これを各要素に設定してあげることにより、UITest はその要素の見た目に関わらず同じ振る舞いをすることができます。
ではやってみましょう。

### コード

```swift
struct ContentView: View {
    @State private var toDetailView = false
    var body: some View {
        NavigationView {
            VStack {
                Button("Go to detail") {
                    toDetailView = true
                }
                .accessibility(identifier: "cloud.mokumoku.uitestsample.contentview.button")
                NavigationLink(
                    destination: DetailView(),
                    isActive: $toDetailView,
                    label: { EmptyView() })
            }
            .navigationBarTitle("Content")
        }
    }
}

struct DetailView: View {
    var body: some View {
        VStack {
            Text("This is detail view")
                .accessibility(identifier: "cloud.mokumoku.uitestsample.detail.text")
        }
        .navigationBarTitle("Detail")
    }
}
```

### テスト

```swift
final class ContentViewUITests: XCTestCase {
    func testMoveToDetailView() {
        let app = XCUIApplication()
        XCTContext.runActivity(named: "Launch app") { _ in
            app.launch()
        }
        XCTContext.runActivity(named: "Check content view") { _ in
            XCTAssertTrue(app.navigationBars["Content"].exists)
        }
        XCTContext.runActivity(named: "Tap button and show detail view") { _ in
            app.buttons["cloud.mokumoku.uitestsample.contentview.button"].tap()
            XCTAssertTrue(app.navigationBars["Detail"].exists)
            XCTAssertTrue(app.staticTexts["cloud.mokumoku.uitestsample.detail.text"].exists)
        }
    }
}
```

SwiftUI はわかりやすいですね。

このようにして AccessibilityIdentifier を設定してあげると文言修正や場所変更に捉われず安定したテストを作ることができます。

## そうは言っても

いきなり Identifier を設定するなんて、どこからやったらいいかわからないし、既存のコードがそういう作りになってなくてやりづらいんですけど…

というのが現実かと思われます。

大丈夫です。 Identifier はなくてもいいので、まずは書くこと、カバレッジを 1% でも上げることが大事だと思います。

by 半年 UITest を書いている人より

## 次回予告

実際のアプリはもっと要素が多くて複雑です。
このままだと一つ一つのテストが長くなり、理解に時間がかかります。
次回はもうちょっと踏み込んで体系的に UITest を書けるようにしていきましょう。

明日は @daichiro です。