UITestの導入とチートシート

# はじまり
Xcode7から導入されたUITestingですが、そろそろiOS9専用の機能も増えてきているし本格的に社内のプロジェクトで使ってみてもいいかなあなんて思っている方も多いのではないでしょうか。今回は導入から良く使うコードのチートシートをまとめてみました。つらいテスト作業を少しでも少なくするお手伝いになれば幸いです。

# UITestの導入

## ターゲットの追加
既存のアプリがObjective-Cで書かれているものでもUnitTestのようにSwiftでテストが書けます。迷わず**Swiftで導入**してください。[こちら](http://qiita.com/taji-taji/items/c00e5b94376c37f17443#1-%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%ABuitests%E3%82%BF%E3%83%BC%E3%82%B2%E3%83%83%E3%83%88%E3%82%92%E8%BF%BD%E5%8A%A0)の記事が丁寧でわかりやすいかと思います。

## 導入時の注意点
上記の方法ですぐにテストが実行できれば問題ないのですが、私の環境では以下のようなエラーが出ました。

```
Undefined symbols for architecture x86_64:
"_main", referenced from:
     implicit entry/start for main executable
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
```

ひとつ目のエラーはアプリケーションテストを行う為のセットアップが済んでいないと出ます。  UnitTestのドキュメント[Xcode Unit Testing Guide](https://developer.apple.com/legacy/library/documentation/DeveloperTools/Conceptual/UnitTesting/02-Setting_Up_Unit_Tests_in_a_Project/setting_up.html#//apple_ref/doc/uid/TP40002143-CH3-SW1)に書いてありました。

1. XcodeでUITestターゲットの「Build Setting」を開く
2. スコープバーをAllにしてBundle Loaderに下記の値を入れる  

```
 BUNDLE_LOADER = $(BUILT_PRODUCTS_DIR)/<app_name>.app/<app_name>
```

<img width="700" alt="スクリーンショット 2016-03-04 19.17.47.png" src="https://qiita-image-store.s3.amazonaws.com/0/83361/c4c8ff63-0244-6c49-2320-c353a0d8c872.png">


ふたつ目のエラーはバイナリタイプのせいみたいです。よくわからない。デバッグとテストをバンドルに変更してあげると消えました。

1. XcodeでUITestターゲットの「Build Setting」を開く
2. Mach-O TypeのDebugに下記の値を入れる  

```
MACH_O_TYPE = mh_bundle
```

<img width="700" alt="スクリーンショット 2016-03-04 19.18.58.png" src="https://qiita-image-store.s3.amazonaws.com/0/83361/b48bfc23-b45d-4035-9991-040e149146a3.png">


# UITestの概要

## XCUIApplication
こいつはアプリケーションそのものを示すクラスです。まずデフォルトで追加される`$(PRODUCT_NAME)`ファイルの`setUp`メソッドを見てみましょう。

```swift:SomeAppUITests.swift
override func setUp() {
        super.setUp()

        // In UI tests it is usually best to stop immediately when a failure occurs.
        continueAfterFailure = false

        // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method.
        XCUIApplication().launch()
    }
```

XCTestCaseのプロパティである`continueAfterFailure`はfalseを入れることでFailureだった場合そこで止まります。UnitTestではメソッド単位で独立したテストを行うのでデフォルトのtrueで使っていたと思いますが、UITestの場合一貫した画面操作なのでどこかでFailureになった時点で終了すべきなのだと思います。

次に出てきた`XCUIApplication()`はUITestで使用する最も重要なクラスで、テスト対象のアプリケーションそのものを指し、`.launch()`で起動しています。アプリの画面を操作する時はこのクラスが継承している`UIAElement`を使って要素を探し出し、操作するといった記述をしていきます。

## UIAElement
自動テストをする上でアプリケーションがもつボタンやテーブルビューなどの全てのユーザインタフェースの親クラス。こいつに存在確認をしたり、タップやスクロールなどの操作を加えることができます。

### 要素の特定
[リファレンス](https://developer.apple.com/library/prerelease/ios/documentation/ToolsLanguages/Reference/UIAElementClassReferenc)を見れば分かる通り、アプリケーションから取り出せるの要素(UIKitのクラス)は通常`UIAElementArray`という`UIAElement`の集合であり、その中から操作をしたいものを特定する必要が有ります。"UIA"は"UIAutomation"ですね。

一番単純な方法としては静的なテキストから要素を特定する方法です。ここにアプリケーションの画面上に"OK"と表示されているボタンがあります。

<img width="500" alt="スクリーンショット 2016-03-05 22.33.56.png" src="https://qiita-image-store.s3.amazonaws.com/0/83361/fc72467b-9631-eb54-97ba-5b0f3bc25b4b.png">

```swift
// ボタンテキストから特定する(staticTextsは省略可能)
XCUIApplication().buttons.staticTexts["OK"]
```

テキストのない画像だけボタンの場合はAssetsに登録されている画像名でも特定が可能です。特定の方法はテキストと同じです。

<img width="700" alt="スクリーンショット 2016-03-04 19.09.13.png" src="https://qiita-image-store.s3.amazonaws.com/0/83361/24572702-7b2b-ab4c-a4f7-4c38c5db25fd.png">


```swift
// 画像名から特定する
XCUIApplication().buttons["thmbs ok"]
```

しかしこれではテキストが可変だった場合や、画像名を変更した場合正常に動作しません。そこでIdentityInspectorにあるAccessibilityのIdentifierを設定することでテキストや画像名に代替して使用できます。

<img width="700" alt="スクリーンショット 2016-03-04 17.26.55.png" src="https://qiita-image-store.s3.amazonaws.com/0/83361/60f0b9c6-8b30-b03c-4c04-65271555d10a.png">


コードの場合はボタンのインスタンスに対して下記のように指定します。あまり書きたくないですね。

```swift
thumbsButton.accessibilityIdentifier = "Thumbs Button"
```

特定の方法はテキストと同じです。

```swift
// Identifierから特定する
XCUIApplication().buttons["Thumbs Button"]
```

一番ハマったのはテーブルビューなどでセル内のContentViewにある要素を特定したい場合です。その要素自体にIdentifierが設定すれば良いのですが、動的に生成されるセルの中身に一々上記のコードを書くのは嫌です。

<img width="500" alt="スクリーンショット 2016-03-05 22.54.07.png" src="https://qiita-image-store.s3.amazonaws.com/0/83361/130ee314-922c-c771-4c35-c77928537fa2.png">

ここで🍕のラベルを持つセルのテキストフィールドに値を入力するためタップする場合はこのようにしました。

```swift
// 子のビューのエレメントタイプから特定する
XCUIApplication().tables.cells.containingType(.StaticText, identifier:"🍕").childrenMatchingType(.TextField).element.tap()
```

また、個人的に面白いと感じたのですが`XCUIApplication`は要素間の階層を自動的に補完してくれます。下記のコードは全て同じ動作をします。

```swift
// セルの中のボタンをタップする
XCUIApplication().tables.cells.buttons["Identifier"].tap()

// "Identifier"が画面内で要素を特定できる唯一のものであれば
XCUIApplication().buttons["Identifier"].tap()

// そのUIAElementが唯一のもの(ボタンが画面内にひとつ)であれば
XCUIApplication().buttons.element.tap()
```

# チートシート
ここではテストで良く使う`XCUIElement`や`XCUIElementQuery`などのメソッドを紹介していきます。

## 要素やデバイスの操作
### タップ
ボタンをタップ、セルをタップ、テキストフィールドにフォーカスするなどあらゆる場面で使います。ロングタップの場合は秒数を指定できます。

```swift
let button = XCUIApplication().buttons["Button"]

button.tap()
button.tapWithNumberOfTaps(5)
button.pressForDuration(5)
```

### スワイプ
いい感じにスワイプしてくれます。テーブルビューの場合は1画面分スワイプしてくれる感じでした。

```swift
XCUIApplication().tables.cells["Cell"].swipeUp()
```

### ドラッグ
`pressForDuration:thenDragToElement:`を使います。セルの入れ替えの場合は指定したセル要素の上までドラッグすれば入れ替わります。

```swift
let tables = XCUIApplication().tables
let topCell = tables.cells["Top Cell"]
let bottomCell = tables.cells["Bottom Cell"]

bottomCell.pressForDuration(0.5, thenDragToElement: topCell)
```

### テキストを入力
上記のタップ操作をしてから入力することができます。

```swift
XCUIApplication().textFields["Name"].typeText("John Smith")
```

### インデックスで指定

```swift
XCUIApplication().tables.elementBoundByIndex(0)
XCUIApplication().tabBars.elementBoundByIndex(1)
XCUIApplication().toolbars.elementBoundByIndex(2)
```

### スライダーの操作

```swift
XCUIApplication().sliders["Percentage"].adjustToNormalizedSliderPosition(0.5)
```

### ピッカーの操作

```swift
XCUIApplication().pickerWheels["Age"].adjustToPickerWheelValue("25")
```

複数のピッカーが並んでいる場合は少し複雑になります。ここで出てくる`NSPredicate`はデータの抽出などで使用する条件(クエリ)を作成できるクラスです。構文については[こちら](http://qiita.com/yusuga/items/8fd531ebd8f5e72bb97b)を参照してください。あまり理解できていないです。

```swift
// "First Picker"で前方一致という条件
let firstPredicate = NSPredicate(format: "label BEGINSWITH 'First Picker'")
let firstPicker = app.pickerWheels.elementMatchingPredicate(firstPredicate)
firstPicker.adjustToPickerWheelValue("first")

// "Second Picker"で前方一致という条件
let secondPredicate = NSPredicate(format: "label BEGINSWITH 'Second Picker'")
let secondPicker = app.pickerWheels.elementMatchingPredicate(secondPredicate)
secondPicker.adjustToPickerWheelValue("second")
```

### ウェブビューのリンクタップ

```swift
XCUIApplication().links["Top"].tap()
```

### リフレッシュ操作
リロードなどをする時に行う引っ張ってリフレッシュする操作はSpriteKitで使用する`CGVectorMake()`と`pressForDuration:thenDragToCoordinate:`を使用します。最小値は6[m/sec]だそうです。

```swift
let cell = XCUIApplication().tables.cells["Cell"]

// 縦方向に重力ゼロと6[m/s]のXCUICoordinateを生成
let start = cell.coordinateWithNormalizedOffset(CGVectorMake(0, 0))
let finish = cell.coordinateWithNormalizedOffset(CGVectorMake(0, 6))

// 上記の重力がかかるまで引っ張る
start.pressForDuration(0, thenDragToCoordinate: finish)
```

### アラートが出た時の処理
アラートが出た時の為に事前に処理を登録しておくことができます。

```swift
// 位置情報の許可アラートが出た時の処理
addUIInterruptionMonitorWithDescription("Location Services") { (alert) -> Bool in
  alert.buttons["Allow"].tap()
  return true
}

// 地図画面に移動して上記の処理を実行
XCUIApplication().buttons["Map"].tap()
XCUIApplication().tap()
```

### キーボードの操作
テキストフィールドなどでキーボードが出ている状態で使えます。`buttons`と`keys`が別になっているのが気持ち悪いですね。ちなみに`app.keyboards`が通常の指定の仕方です。

<img width="500" alt="スクリーンショット 2016-03-06 0.46.05.png" src="https://qiita-image-store.s3.amazonaws.com/0/83361/d40f7659-7122-8b5f-9b5e-915f31f48191.png">

```swift
let app = XCUIApplication()

app.buttons["Return"].tap()
app.buttons["Next keyboard"].tap()
app.keys["more, letters"].tap()
app.keys["more, numbers"].tap()
app.keys["delete"].tap()
app.keys["shift"].tap()
app.otherElements["good"].tap()
app.buttons["Hide keyboard"].tap()
```

### デバイスの操作

```swift
// ホームボタンを押下
XCUIDevice.sharedDevice().pressButton(XCUIDeviceButton.Home)

// デバイスを左回転
XCUIDevice.sharedDevice().orientation = .LandscapeLeft
```

## 要素のテスト
### 存在確認
UITestの中で一番基本になるテストだと思います。要素が存在すればtrueが返ってきます。

```swift
// "Button"のIdentifierを持ったボタンが存在するか
XCTAssert(XCUIApplication().buttons["Button"].exists)

// プッシュ先でどの画面かナビゲーションバーのタイトルで確認する
XCTAssert(app.navigationBars["Title"].exists)
```

### 表示確認
上記の`exists`だと画面外で表示されていてもtrueが返ってきてしまします。ここで画面にちゃんと表示されているかを確認するには`CGRectContainsRect()`を使用します。

```swift
let app = XCUIApplication()

// ウィンドウ(UIKitだとUIWindow)を取得
let window = app.windows.elementBoundByIndex(0)

// ボタンがウィンドウのフレーム内に含まれているか
let element = app.buttons["Button"]
XCTAssert(CGRectContainsRect(window.frame, element.frame))
```

## 非同期処理
読み込み画面が出るなどの非同期処理が走る場合にある条件を満たすまで待つことができます。ピッカーで出てきた`NSPredicate`を使っています。

```swift
// くるくる画像がいなくなるまでという条件
let waitingImage = self.app.images["Waiting"]
let notExists = NSPredicate(format: "exists == false")

// 検索してくるくるがいなくなるまで5秒間処理を中断する
expectationForPredicate(notExists, evaluatedWithObject: waitingImage, handler: nil)
app.buttons["Search"].tap()
waitForExpectationsWithTimeout(5, handler: nil)
```

ちなみに`waitForExpectationsWithTimeout`で指定した時間内に条件を満たさなかった場合Failureとなりテストが終了します。

## その他
### デバッグ
シミュレータ内でGeneral > Accessibilityの Accessibility Inspectorを使うと要素のIdentifierやフレームなどを確認できます。

<img width="500" alt="スクリーンショット 2016-03-06 2.03.46.png" src="https://qiita-image-store.s3.amazonaws.com/0/83361/0115af05-5b4b-801d-654d-b46471cdf9c7.png">

<img width="500" alt="スクリーンショット 2016-03-06 2.04.35.png" src="https://qiita-image-store.s3.amazonaws.com/0/83361/9ab088de-b6a3-40ee-509b-14d42507d407.png">


上記を立ち上げると操作が面倒なので、ちょっと見たい時などはコンソールに出力することもできます。

```swift
print(XCUIApplication().buttons["Button"].debugDescription)
```

# おしまい
ここまでズラズラと書いてきましたが、Identifierがしっかりと設定されていればチートシートにある大部分はレコーディング機能を使用すれば自動で記述してくれます。日本語ダメだったり、結構わけわからないことするのでリファクタリングは必須ですが…
あと機能の改修を行った際にはほぼ間違いなくUITestのコードも改修が必要になると思われます。この修正に当たるリソースと人力で行うテスト作業のリソースを比べてうまく運用していかないと本末転倒なことになりそうです。今後のアップデートに期待したいと思います。

# 参考文献
[UIAElement Class Reference](https://developer.apple.com/library/prerelease/ios/documentation/ToolsLanguages/Reference/UIAElementClassReference)
[UIAElementArray Class Reference](https://developer.apple.com/library/prerelease/ios/documentation/ToolsLanguages/Reference/UIAElementArrayClassReference)
[XCTest Reference](http://masilotti.com/xctest-documentation/index.html)
[UI Testing Cheat Sheet](https://github.com/joemasilotti/UI-Testing-Cheat-Sheet#basic-functionality)
[UI Testing Cheat Sheet and Examples](http://masilotti.com/ui-testing-cheat-sheet)