Xcode 8.0 beta & Swift 3.0 を試しながらiOSのUITestに入門

# はじめに
今更感ありますが, `Xcode 7`から登場したiOSのUITestを簡単なアプリを作成して試してみました.

iOSではユニットテストをすることは可能で, カバレッジはそれなりに出せるかとは思いますが, **実機でポチポチやるテストは手動でやる必要がありました.**

`View`をデバッグするとき, 軽微な変更なのにいちいちアプリ立ち上げて, ダメだったらまた書きなおして...の反復をやっていると手動では辛くなってきますよね(´・ω・｀)

そこで, `Xcode 7`からはUITestが可能になり, **自動的に画面を操作してくれて見た目や挙動を確認できる仕組みが整備されました.**
(WebアプリのUIテストツール`selenium`みたいなやつ!)

企業では, QA(Quality Assurance)の人がテスト試験項目書作成して挙動が正しいかどうかをチェックすることがあると思うのですが, UITestによってこの作業も軽減することが出来ると思います**(人件費も削減できますね!).**

ということで, XcodeのUITestについて例を示しながら説明していきます.

ちなみに自分はテストコードを一切書いたことがありませんのでご了承ください(`Swift`ではなく`Rails`のチュートリアルで書いたくらい).

# ソースコード
[iOS-UITestSampleApp](https://github.com/gates1de/iOS-UITestSampleApp)

丁寧に作れませんでした> <

とりあえず挙動だけ知りたい人は, cloneしてプロジェクト開いて`cmd + u`すればよいです.

# 必須環境
* Mac OS X 10.11.4以降
* **Xcode 8.0 beta**
* **Swift 3.0**

※ `Swift 3.0`で書かれていることに注意してください! 2.2からそれなりに違いますし, `Xcode 8.0 beta`以降でないと動きません!

~~というかこれを期にのり換えてください~~

**ちなみにXcode 8.0 beta, 結構クラッシュしますw** (個人の環境によると思いますが笑)

# 実装したもの
自分が思いつくままに適当な機能を詰め込みました.

* ログイン(嘘)
* 機能選択UI
* 2乗&根号計算機能
* 郵便番号検索機能

また, UITestの実装が大体つかめるように, 使う必要もないような部品も置いています.

画面の説明を以下に記載していきます.


## トップ画面
こちらはログインボタンのみが存在する画面です.

ここでは`UIButton`がちゃんと存在しているかをテストします.


## WebViewでのログイン認証画面
強制的にログインが成功するリンクのみ置いています.

次の画面に遷移 = ログイン成功として, ログインが成功することをテストします.

`WebView`を使っているので, `WebView`の部品取得も少し紹介します.

また, どの機能が選択されたのかもここでテストすることにします.


## モーダルで出現する機能選択画面
2つの機能のどちらかを選択してもらう画面です.

ここではcellがちゃんと2つあることと, 機能選択したら画面が消えることをテストします.

`Unwind segue`で戻るので, 上に書いたように機能選択テストはログイン画面でおこないます.


## 2乗と根号の計算画面
`textField`に数値を入力・計算ボタンを押すと, その数値の2乗した値とルートの値を算出する機能が使える画面です.

ちゃんと計算が合っているかをテストします.


## 郵便番号で地域検索画面
`textField`に郵便番号を入力・検索ボタンを押すと, その郵便番号に該当する都道府県名・市区町村名・町域名が表示される画面です.

検索結果が合っているかを確認, 加えて非同期処理におけるテストを書いています.

---

といったラインナップで解説していきます!


# テストコードの説明

## トップ画面

### ログインボタン存在テスト
ログインボタンが存在しているかのテストで, コードは以下の通りです.

```swift:WelcomeViewControllerUITests.swift
func testIsExistsLoginButton() {
    let app = XCUIApplication()

    let loginButton = app.buttons["LoginButton"]

    XCTAssertTrue(loginButton.exists)
}
```

まず, `XCUIApplication()`とはアプリケーションのプロキシ(代理)のことです.

アプリケーションの情報を持っていて部品を操作することが可能, そしてアプリケーションの起動と終了もしてくれます.

勝手に画面を動かせるのはこのクラスのお陰なのではないでしょうか.

このクラスは`Element`を持っています.

指定の`UIButton`を取得したいときは上記のように,
`app.buttons["accessibility identifier"]`
と書きます.

ここで, `accessibility identifier`ってなんぞや? という人のために設定方法を紹介します.

<img width="1393" alt="LoginButton.png" src="https://qiita-image-store.s3.amazonaws.com/0/61124/81ee96d1-81fb-305b-2ae2-08b78223751c.png">

対象画面の`Storyboard(Interface Builder)`を開いて, 対象のボタンを選択します.

今回は`Main.storyboard`の`LoginButton`を選択します.

画面右の`Utilities`の中にある`Identity inspector`(新聞のアイコンみたいなやつ)を選択すると, 下の方に`Accessibility`という項目があります.
ここの`Identifier`に, 部品の識別子を設定しましょう.

このようにして部品を取得することが可能です.

`XCTAssertTrue`はテストでお馴染みのメソッドで, 引数にした値が`true`であればテストが成功になります.

ということで, このテストコードはログインボタンが存在すればテスト成功ということになります.


#### テスト実行
それでは`WelcomeViewControllerUITests.swift`の`func testIsExistsLoginButton()`の横にある菱型◇を押してみましょう. (もしくは, クラスを定義した一番最初の行あたりにある菱型◇)

<img width="428" alt="TestStart.png" src="https://qiita-image-store.s3.amazonaws.com/0/61124/2367c026-57bf-54cd-f647-edddeafcc7d0.png">


ビルドに成功してテストが走るとシミュレータの画面が立ち上がります.

今回のテストは動いているように見えませんが, 本来勝手に動いて操作してくれます.

ログインボタンが存在していることが確認できたということで, `loginButton.exists`は`true`となり, テストが通るはずです.


## ログイン画面

### ログイン成功テスト
まずはログインが成功することをテストするコードです.

``` swift:LoginViewControllerUITests.swift
func testLoginSuccess() {
    let app = XCUIApplication()

    let selectFeatureTableView = app.tables["SelectFeatureTableView"]

    login()

    // 画面遷移時間考慮
    // 機能選択画面への遷移をログイン成功とみなす
    expectation(for: Predicate(format: "exists == true"), evaluatedWith: selectFeatureTableView, handler: nil)
    waitForExpectations(withTimeout: 1, handler: nil)
}
```

まずは最初のテストと同様に, 
`StoryBoard`で対象の`tableView`に`accessibility adentifier`を設定すればいいのですが, `tableView`の `Identity inspector`には`Accessibility`の項目が見つかりません.

そんな時は下記画像にあるように, `User Defined Runtime Attributes`に追加すればいいのです.

<img width="1393" alt="SelectFeatureTableView.png" src="https://qiita-image-store.s3.amazonaws.com/0/61124/2c545ee0-8867-2739-396a-b7f7a15aa5d5.png">


＋ボタンを押下して以下の設定をします.

* Key Path = accessibilityIdentifier
* Type = String
* Value = SelectFeatureTableView

これで`app.tables["SelectFeatureTableView"]`とすると, 対象の`tableView`が取得できます.

続いてその下の`login()`というメソッドですが, これは`XCTestCase`を`extension`したことで追加したメソッドになります.

``` swift:LoginViewControllerUITests.swift
extension XCTestCase {

    func login() {
        let app = XCUIApplication()

        let loginButton = app.buttons["LoginButton"]
        let loginSuccessLink = app.links["強制的にログイン成功"]
        loginButton.tap()
        loginSuccessLink.tap()
    }
}
```

ログイン処理などがあるアプリで, 画面ごとに`XCTestCase`を継承したクラスを作成したいとなった時, いちいち書くのは面倒なので共通化しておきます.

ちなみに`app.links["強制的にログイン成功"]`というのが`webView`のリンクを取得する処理になります.

この`webView`は, プロジェクト内の`index.html`を読み込んだもので, その中のリンクは以下のように設定されています.

``` html:index.html
...
<p><a href="native://myapp/login;param?statusCode=200&status=success">強制的にログイン成功</a></p>
...
```

`accessibility identifier`として, aタグで囲んだ文章("強制的にログイン成功")を指定すると取得できます.

そして最後に, 普段コードを書いているうえでは見慣れない表記がありますね.

``` swift
// 画面遷移時間考慮
// 機能選択画面への遷移をログイン成功とみなす
expectation(for: Predicate(format: "exists == true"), evaluatedWith: selectFeatureTableView, handler: nil)
waitForExpectations(withTimeout: 1, handler: nil)
```

このテストコードが意味するところは, **「1秒間待ってselectFeatureTableViewが存在していればテスト成功」**といった感じです.

`expectation()`はその名の通り, 期待する動作を定義するためのメソッドです.

`Predicate`(NSPredicate)とは, `CoreData`などで利用され, 検索条件などを設定するためのクラスのようです.
`NSArray`にも`fitered(using: Predicate)`というメソッドが用意されています.
この点に関しての詳細は今回スコープ外とさせて頂きますので, 調べてみておいてください.

そして, `evaluatedWith`には, `Predicate`の対象となる部品を指定します.

最後に, `waitForExpectations(withTimeout: 1, handler: nil)`で, `expectation()`を1秒間待ち, 条件を判定します.

画面遷移を挟んでいる部分なので, これじゃないとテストが通らないことがあったためにこうしました.
非同期の処理にも使える優れものですね!

#### テスト実行
それでは`func testLoginSuccess()`の横の菱型◇を押してテストを実行してみましょう.
すると今度こそ勝手に画面が動くはずです.

**ログインボタンを押して画面遷移
→「強制的にログイン成功」リンクを押して機能選択画面がモーダルで表示される**

みたいな感じで動いてくれます.

そこに機能選択の`tableView` = `selectFeatureTableView`があるのでテストは成功します.


### 計算機能選択テスト
上でも述べたように「強制的にログイン成功」リンクを押すと, 下から機能選択のための`TableViewController`がモーダルで出てきます.

文字が入っている`cell`をタップすると, `TableViewController`が消えて, ログイン画面から次の画面に遷移します.

まずは1番目の`cell`をタップした時のテストです.

``` swift:LoginViewControllerUITests.swift
func testSelectedCalculator() {
    let app = XCUIApplication()

    let cell = app.cells.matching(.cell, identifier: "ReuseIdentifier").element(boundBy: 0)
    let calculatorTitle = app.staticTexts["CalculatorTitle"]

    login()

    cell.tap()

    expectation(for: Predicate(format: "exists == true"), evaluatedWith: calculatorTitle, handler: nil)
    waitForExpectations(withTimeout: 1, handler: nil)
}
```

まずは`cell`の取り方です.
`cell`は`Identity inspector`にて`accessibility identifier`が設定できますので, 識別子を入力しておきます.

`app.cells.matching(.cell, identifier: "ReuseIdentifier")`
で対象の`cell`を全て取得できるのですが, 全部はいらないので
`.element(boundBy: 0)`で0番目の`cell`を取ってきます.

続いて, `app.staticTexts["CalculatorTitle"]`ですが, これは画面に設置したタイトル専用の`label`を指します.

<img width="1394" alt="CalculatorTitle.png" src="https://qiita-image-store.s3.amazonaws.com/0/61124/007229c5-0aa4-92c3-5833-056cb094acba.png">

`app.labels["CalculatorTitle"]`とかで出来そうな雰囲気ですが, そもそも`app.labels`がありません.
`label`を取るときはとりあえずこれで良いと思います.

最後の3行はログイン成功テストと同様に, 「1秒間待って`calculatorTitle`が存在すればテスト成功」となります.

#### テスト実行
`func testSelectedCalculator()`の横の菱型◇を押してみましょう.

**ログインボタンを押して画面遷移
→「強制的にログイン成功」リンクを押して機能選択画面がモーダルで表示
→ 1つめの`cell`をタップして機能選択画面が消える
→ 計算画面に遷移**

といった感じで動いてくれて, `calculatorTitle`がそこにあるのでテストは成功します.


### 郵便番号で地域検索機能選択テスト
次に, 機能選択画面の2番目の`cell`をタップした時のテストコードです.

``` swift:LoginViewControllerUITests.swift
func testSelectedPostalCodeSearch() {
    let app = XCUIApplication()

    let cell = app.cells.matching(.cell, identifier: "ReuseIdentifier").element(boundBy: 1)
    let postalCodeSearchTitle = app.staticTexts["PostalCodeSearchTitle"]

    login()

    cell.tap()

    expectation(for: Predicate(format: "exists == true"), evaluatedWith: postalCodeSearchTitle, handler: nil)
    waitForExpectations(withTimeout: 1, handler: nil)
}
```

まー, 計算機能選択テストとほぼ一緒なんで説明は省略しますｗ


## 機能選択画面

### `cell`数一致テスト
続いて, 機能選択画面にある`cell`の数が合っているかを確認するテストです.

**ここからはさらっといきます笑**

``` swift:SelectFeatureTableViewControllerUITests.swift
func testSelectFeatureTableViewCellCounts() {
    let app = XCUIApplication()

    let cells = app.cells.matching(.cell, identifier: "ReuseIdentifier")

    login()

    // 画面遷移時間を考慮
    expectation(for: Predicate(format: "count == 2"), evaluatedWith: cells, handler: nil)
    waitForExpectations(withTimeout: 1, handler: nil)
}
```
見た感じもわかりやすいですね.

`Predicate(format: "count == 2")`ということで, `cell`の数が2個であればテストが通ります.

#### テスト実行
**省略**


### 画面遷移確認テスト
`cell`をタップすると本当に画面が消えるのかどうかをテストします.

``` swift:SelectFeatureTableViewControllerUITests.swift
func testDismissSelectFeatureTableView() {
    let app = XCUIApplication()

    let cell = app.cells.matching(.cell, identifier: "ReuseIdentifier").element(boundBy: 1)

    login()

    cell.tap()

    expectation(for: Predicate(format: "exists == false"), evaluatedWith: cell, handler: nil)
    waitForExpectations(withTimeout: 1, handler: nil)
}
```

他のテストとちょっと違っているのは,
`expectation(for: Predicate(format: "exists == false"), evaluatedWith: cell, handler: nil)`
のところで, 画面遷移した後のことなので, `cell`が存在してないということになり, テストは通ります.

#### テスト実行
**省略**


## 計算画面

### 計算結果一致テスト
`textField`に数値を入力して, その値のルート計算結果と2乗計算結果が, 予想される答えと一緒ならテストが通ります.

``` swift:CalculatorViewControllerUITests.swift
func testCalculation() {
    let app = XCUIApplication()

    let cell = app.cells.matching(.cell, identifier: "ReuseIdentifier").element(boundBy: 0)
    let textField = app.textFields["NumberTextField"]
    let calculateButton = app.buttons["Calculate"]
    let rootResult = app.staticTexts["RootResult"]
    let squareResult = app.staticTexts["SquareResult"]

    login()

    cell.tap()
    textField.tap()
    textField.typeText("256")
    calculateButton.tap()

    XCTAssertTrue(rootResult.label == "16.0")
    XCTAssertTrue(squareResult.label == "65536.0")
}
```

新しく`app.textFields`が出てきました.

とはいえこれも同様に, `accessibility identifier`を設定すれば取ってこれます.

`textField.typeText("256")`のところで, `textField`に256という数値をタイピングする操作がおこなわれます.

計算は即時反映なので, `expectation()`と`waitForExpectations()`を使わなくても, `XCTAssertTrue()`で十分です.

#### テスト実行
**省略**


## 郵便番号検索画面

### 検索結果一致テスト
`textField`に郵便番号を入力して, 出力された検索結果と予想される検索結果が一緒ならテストが通ります.

ここでは非同期処理がおこなわれます.

``` swift:PostalCodeSearchViewControllerUITests.swift
func testPostalCodeSearch() {
    let app = XCUIApplication()

    let navigationBar = app.navigationBars["PostalCodeSearchNavigationBar"]
    let cell = app.cells.matching(.cell, identifier: "ReuseIdentifier").element(boundBy: 1)
    let textField = app.textFields["PostalCodeTextField"]
    let searchButton = app.buttons["Search"]
    let searchResult = app.staticTexts["SearchResult"]

    login()

    cell.tap()
    textField.tap()
    textField.typeText("1600022")
    searchButton.tap()

    XCTAssertFalse(navigationBar.exists)
    // 非同期処理を挟むので3秒くらい余裕をとる
    expectation(for: Predicate(format: "label == '東京都新宿区新宿'"), evaluatedWith: searchResult, handler: nil)
    waitForExpectations(withTimeout: 3, handler: nil)
}
```

ちょっと関係ないのですが, 計算画面とこの画面では`navigationBar`が隠されるので, そのテストもおこなっています. 参考までに.

非同期通信が入った場合でも, `expectation()`と`waitForExpectations()`を駆使すれば簡単にテストできます.

今回は検索結果が出るまで少々時間があるので, 3秒間待つことにしています.

#### テスト実行
テストが動いている様子を`gif`でお送りします.

※ マウスカーソルがないところを見ると, 本当に勝手に動いているのがわかりますね!

![TestPostalCodeSearch.gif](https://qiita-image-store.s3.amazonaws.com/0/61124/ebf9a6cf-a0b3-3b71-d949-63d6cd119db3.gif)

## 番外編

### Record UI Test
ここまでUITestで部品を取得したり, 操作手順などを考えて書いてきました.

実はもうちょっと楽にできる方法がありまして, それが`Record UI Test`というやつです.

下の画像の一番下に赤い丸が見えますよね.
そして`func testRecordUITest()`が作成されてますが, 中には何も書かれていません.

<img width="878" alt="RecordUITest.png" src="https://qiita-image-store.s3.amazonaws.com/0/61124/98aa9858-afab-e7b5-be8e-0bbf2b96a76d.png">

そこで, `func testRecordUITest()`のスコープの中(47行目)にフォーカスを当てて赤い丸を押すと, アプリのビルド&起動が始まります.

そのままアプリを操作すると, **なんと! アクションを起こしたUIの処理が自動生成されます.**

どういうことかというと, 例えばログインボタンを押下すると
`XCUIApplication().buttons["LoginButton"].tap()`
みたいなのが自動生成されるということです.

ちょっと`Xcode 8.0 beta`の調子が悪くて, `webView`でバグってしまうので操作をお見せすることが出来ないのですが, 生成されるテストコード例は以下のとおりです.

``` swift
func testRecordUITest() {
    let app = XCUIApplication()

    XCUIApplication().buttons["LoginButton"].tap()
    app.links["強制的にログイン成功"].tap() // ここらへんは手動で書きました.

    app.tables["SelectFeatureTableView"].staticTexts["Postal Code Search"].tap()

    let postalcodetextfieldTextField = app.textFields["PostalCodeTextField"]
    postalcodetextfieldTextField.tap()
    postalcodetextfieldTextField.typeText("1160002")
    app.buttons["Search"].tap()
}
```

命名がアレですが, 部品の取得と操作が自動生成されました.

メリットはあるのですが, アプリの`View`が増えてきたときにこれを実行すると, 結構無駄なネストで部品を取得しようとします
(`app.windows.matching(identifier: "hoge").element(boundBy: 0).buttons["hoge"]...`みたいな感じ).

コードの可読性も求めるなら手動でやっていくのも有りかと思います.

`webView`などは部品が取りにくいので, 一度`Record UI Test`を実行してタップなどをしてみたりすると良いかもしれませんね!

### Dispatch
**「x秒後に処理を走らせたい!」**なんてことはよくありますよね!

`Swift 2.2`までは以下の記述で可能でした([参考](http://qiita.com/k-yamada@github/items/d124e96e975906b12990)).

``` swift
let delay = 1.0 * Double(NSEC_PER_SEC)
let time  = dispatch_time(DISPATCH_TIME_NOW, Int64(delay))
dispatch_after(time, dispatch_get_main_queue(), {
  // 1秒後実行
})
```

`Swift 3.0`ではこの書き方はエラーが起きて, `Dispatch~`というクラスが多数作成されているのでそちらを使うことになります. 以下に同じ挙動の例を示します.

``` swift
DispatchQueue.main.after(when: .now() + 1.0, execute: {
  // 1秒後実行
})
```

スッキリ!

今後`Dispatch~`の中身について調べていこうかと思います.


# おわりに
ちょっと説明が長くなってしまって申し訳ないです> <

でもiOSにおけるUITestのイメージが大体つかめたんじゃないかと思います.

ユニットテストだけでは出せないカバレッジも出せると思うので, アプリの品質向上のためにもなりますし, 人がやる作業を減らすためにも, UITestをぜひ始めましょう!

また, アドバイスがありましたらコメントよろしくお願いいたします!
それでは〜〜〜