Xcode7から導入されたXCTestのUI recordingで遊んでみる

# はじめに
2015年6月のWWDC2015でiOS9とかSwift 2.0とかが発表されましたが、
それらを扱うためのXcodeの新バージョンであるXcode7も発表されました。
[Xcode7の新機能](https://developer.apple.com/library/prerelease/ios/documentation/DeveloperTools/Conceptual/WhatsNewXcode/Articles/xcode_7_0.html)の中でも、個人的には**UI recording**が特に気になったので、簡単に触ってみました。

最初に言っておくと、
「XcodeでSelenium IDEのように画面操作を記録しつつ、Selenium WebDriverのようにテストコードを記述・実行できる」
という内容です。

# テスト対象のアプリを作る

使用感を確かめるために、まずはテストアプリとして[ボタンを押すと数字がカウントアップするアプリ](https://github.com/IsaoTakahashi/swift_countup)を作りました。
_(2020-03-10 追記: Swift5にMigrationしました)_

「+1」ボタンをタップすると、その回数が表示されます。
![スクリーンショット 2015-06-13 17.16.21.png](https://qiita-image-store.s3.amazonaws.com/0/72808/24b00b52-464c-9d11-0a34-ae7e84f66f21.png)

# UIの動作をテストする
アプリができたので、早速テストを作ってみましょう。
## Test Navigatorのタブでテストを選ぶ
![test_navigator.png](https://qiita-image-store.s3.amazonaws.com/0/72808/8c8fd24a-9011-307e-381d-01271a4f9cf0.png)
Test Navigatorのタブでは、作成したテストケースがツリー上に表示されます。
(これはXcodeプロジェクト作成時に「Include Unit Tests」「Include UI Tests」を選択すると自動生成されるテストケースです)

今回はCountUpUITests.TestExampleに対してテストコードを書いていきます。

## UI上の操作を記録する
下の図のように「UIの操作を行うコード」を挿入したい位置にカーソルを合わせた状態で、
録画ボタンを押すと、アプリケーションが立ち上がり記録可能状態になります。
(既にアプリケーションが立ち上がっている場合、その状態から記録が始まります)
![record_button.png](https://qiita-image-store.s3.amazonaws.com/0/72808/706f7c9c-90bd-c74b-4abf-d7f2c4d6cce5.png)

では早速「+1」のボタンをタップしてみると…
![click_1.png](https://qiita-image-store.s3.amazonaws.com/0/72808/65d2608a-e76a-d7b3-f851-eb998374b69c.png)
こんな風に、

``` swift
XCUIApplication().buttons["+ 1"].tap()
```
というコードが挿入されました。
もう一度タップしてみると、↓のようなコードが生成されます。

``` swift
let 1Button = XCUIApplication().buttons["+ 1"]
1Button.tap()
1Button.tap()
```
若干イイ感じにリファクタリングしてくれました。
(が、文法的にNGな変数が作られるので、手動で修正する必要があります。
 Xcodeが自動でリファクタリングするのに、その結果に対してXcodeがエラー吐くとか…)

あと、`UILabel`をタップしたときのイベントも記録できます。

``` swift
app.staticTexts["2"] //操作時のラベルのテキストの内容がキーとして扱われる
```
ラベルテキストをキーとして用いるのはアレなので、
実運用では、他の方法(後述)で対象オブジェクトを同定して扱うことになると思います。

## 記録を終了する
記録を終了するには、録画ボタンを再度押せばOKです。
(*記録中は、記録により生成されたコードをいじることはできないようです)


## 記録したテストの実行
記録ができたので、Assertionを入れて実際にテストを動かしてみます。

### Assertionの追加
せっかくのテストなので、テキトーにAssertionを入れておきます。

``` swift:ボタンを2回タップしたときのラベル表示を検証するテスト
func testExample() {
    let app = XCUIApplication()
    let staticText = app.staticTexts.elementAtIndex(0)
    let button = app.buttons["+ 1"] //"+1"ボタン

    XCTAssertTrue(staticText.label == "0") //起動時のラベルは"0"
    button.tap()
    button.tap()
    XCTAssertTrue(staticText.label == "2") //ボタンを2回タップしたので、ラベルは"2"になっているはず
}
```
staticTextの取得方法のところで、`app.staticTexts.elementAtIndex(0)`としています。
これによって、ラベルのテキスト内容にかかわらず0番目のラベルを取得できます。
("さっきよりマシ"程度の取得方法です)

### UIテストの実行
というわけで、テストを実行してみましょう！
(テストケースのLine number左側に「◇」アイコンがあるので、それをクリックすればテストケースが実行されます)

実際にテストを実行した動画がこちら。
![ui_test.png](https://qiita-image-store.s3.amazonaws.com/0/72808/60000504-07d7-71b2-9528-7747b5dbb14f.png)
[Xcode7 UI Testing](https://youtu.be/xEt93e3LjOU)
(動画では見えていませんが、テスト終了後に「Test Succeeded」のポップアップが出ています)

こんな風に、UIの自動テストができちゃいます！
既存のiOS向けサードパーティ製のUIテストフレームワークはあまり触ったことがないのでちゃんとした比較はできませんが、
まぁ大体Selenium(WebDriver)と似たような感じで使えるのかなーと。
何より、一つのツールでコーディング・デバッグ・単体テスト・UIテストすべてを実行できるのは非常に助かります。

## UIオブジェクトをもっと賢く取得する
さきほどまでは、テスト対象のオブジェクトを取得する方法がかなりテキトーだったので、
CSSのid指定のように対象オブジェクトを狙い撃ちで取得できるように修正を入れます。

### やること
テスト時に取得したいオブジェクトについて、Identifierを指定するだけです。
StoryBoard(IB)・コードそれぞれで指定が可能です。

#### StoryBoard(Interface Builder)を用いる方法
対象のオブジェクトを選択してIdentity Inspectorを開いた後、
AccessibilityグループのIdentifierに、ユニークな文字列を指定します。
![identifer.png](https://qiita-image-store.s3.amazonaws.com/0/72808/36835d18-68d8-ef12-b3ca-4da8aa849091.png)


#### コードに直接記述する方法
対象のオブジェクトの`accessibilityIdentifier`にユニークな文字列を指定します。

```swift
countLabel.accessibilityIdentifier = "testLabel"
```

### テストコードを書き直す
Identifierを指定したら、それをキーにしてオブジェクトを取得できるように修正します。

``` swift
func testExample() {
    let app = XCUIApplication()
    let staticText = app.staticTexts["testLabel"]
    let button = app.buttons["testButton"]

    XCTAssertTrue(staticText.label == "0") //起動時のラベルは"0"
    button.tap()
    button.tap()
    XCTAssertTrue(staticText.label == "2") //ボタンを2回タップしたので、ラベルは"2"になっているはず
}
```

ここまで来ると、だいぶキレイな感じになりますね。

### 補足
最初からIdentifierを指定しておけば、記録時にちゃんとIdentifierを認識してコードを生成してくれます。
また、その際は変数名が「`Identifier`+`ElementType`」となるようで、
上述したような「自動リファクタリングでエラーになる」という謎な状態にはなりません。

``` swift:Identifierを指定している状態で記録を行った場合
let testbuttonButton = app.buttons["testButton"]
testbuttonButton.tap()
testbuttonButton.tap()
```

# おわりに
Xcode7から導入されたUI recordingの機能を使って、簡単にUIテストを作ってみました。
今回はUIButtonとUILabelのみを使用しましたが、`XCUIElementType`の定義を見ると多様なオブジェクトを扱えるようです。
また、オブジェクトの取得についても`XCUIElementQuery`に定義されているメソッドを見ると様々な方法で行えるようです。

現時点ではこれらのクラスのリファレンスやサンプルコードはAppleから提供されていないみたいなのでほぼ完全に手探りになりますが、それなりに感覚的に利用できるのでそこまで困ることはなさそうです。