【まとめ】What's New in Testing【WWDC 2017】

# はじめに

この記事では、WWDC 2017でのセッション [What's New in Testing](https://developer.apple.com/videos/play/wwdc2017/409/) の内容をまとめます。
このセッションでは、Xcode9から登場する多種多様な新しいAPIが発表され、UI Testのパフォーマンス改善などの技術的な進化も紹介されました。
macOSやtvOSなどに関するトピックもありましたが、僕自身がそれらに詳しくないのと、発表のほとんどがiOS関連だったので、iOS関連のトピックに関してのまとめになります。
Xcode9にアップデートして強化されたUI Testを利用したい方や、これからUI Testを導入しようと思っている方の参考になれば嬉しいです。

それでは、早速まとめに入りましょう。

# セッションのまとめ

## Xcode9でのEnhancements

### 並列テスト

- 複数のシナリオを1回のビルドで並行してテストできるようになる。
- テスト実行スピードが大幅に上がる。

### 言語と地域の指定(Localization)

- テストを走らせるときに言語と地域を指定できるようになる。

## Async testing(非同期処理のテスト)

### `expectation`や`waitForExpectation`などについて
- Xcode6から提供されているAPI
- 非同期処理の完了を待機するために用いられる。
- 問題点
  - タイムアウトがすべてテスト失敗と扱われる
  - テストオブジェクト自体を待つからテスト失敗の原因特定が難しい
  - 待機をネストできない

今まではこんな感じでテストしていました。

```swift
let document = UIDocument(fileURL: documentURL)
let documentExpectation = expectation(description: "Document opened")
document.open() { success in
  XCTAssert(success, "Failed to open file")
  documentExpectation.fulfill()
}
waitForExpectations(timeout: 10)
```

### `XCTWaiter`がXcode9から登場。
次のようなことが可能になった。
- 待機リストの管理
- `XCTWaiterDelegate`へのコールバック

今まではすべての`expectation`に対してぼんやりと待機していた。

```swift
// Test case waits implicitly
waitForExpectations(timeout: 10)
```

Xcode9では次のようにフレキシブルにテストを処理できる。

```swift
// expectationを指定してテストケースをwaitさせられる
wait(for: [documentExpectation], timeout: 10)

// コールバック処理が書ける
XCTWaiter(delegate: self).wait(for: [documentExpectation], timeout: 10)

// waitの返り値によって処理を分けられる
let result = XCTWaiter.wait(for: [documentExpectation], timeout: 10)
if result == .timedOut {
  // handling the timeout…
}
```

### `XCTestExpectation`も進化

次のように変更された。

- initializerが`XCTest`から分離され、`public`に。
- 複数回`fulfill`される`expectation`にその回数を保持するプロパティが登場。
- タイムアウトまでに`fulfill`されてしまうと逆にテストが失敗するInverted behaviorが登場。
- オプショナルなフラグを利用して`fulfill`される順番も指定できる。

## Multi-app testing

### `XCUIApplication`について

- このクラスのインスタンスを利用すると、アプリの起動・終了やUI部品などへのクエリ作成、タップなどの同期イベントなどをシミュレーションできて便利。
  - しかし、Test Targetを指定しているので他のアプリケーションをテストすることができなかった。

### Xcode9での`XCUIApplication`

- 複数のアプリを起動し、アプリ間でデータの受け渡しが可能に。
- 複数の設定で複数のシナリオをテストできるように。

上記を実現するために次のような変更が行われた。

- `init(bundlerIdentifier: String)`
  - bundle IDを指定してアプリを起動するInitializerが登場。
- `func activate()`
  - バックグラウンドからフォアグラウンドにアプリを持ってくる、もしくは起動されていなかったらそのアプリの`XCUIApplication`インスタンスを作成するメソッドが登場。
- `var state: XCUIApplication.State { get }`
  - アプリの今の状態を保持するプロパティが登場。

このように複数アプリを起動でき、アプリ間の連携も可能に。

```swift
let readerApp = XCUIApplication(bundleIdentifier: "com.mycompany.Reader")
let writerApp = XCUIApplication(bundleIdentifier: "com.mycompany.Writer")

readerApp.launch()
// interact with first app
writerApp.launch()
// interact with second app
readerApp.activate()
// return to first app without relaunching
```

### デモ

デモでは、新登場のAPIを使用してメッセージをサーバへ送信するアプリとサーバからメッセージを受け取るアプリの2つのアプリを同時にテストしていました。
少しのAPI追加でここまでテストの幅が広がるのはすごいですね。

## UI testing performance(UI Testのパフォーマンス改善)

### クエリの仕組みとその問題点

UI部品を検索する際の手順は次のようになっている。

1. test processがアプリケーションへ現在のデータのスナップショットをリクエスト
2. アプリケーションはデータのスナップショットをキャプチャし、シリアライズしてtest processに返す
3. test processが受け取ったスナップショットを開封し、クエリを用いてUI部品を検索する

この問題点には以下のようなものがある。

- スナップショットをキャプチャするのに時間がかかる
  - tableに数千のrowがあったり、巨大なcollection viewがあったりするとテスト失敗を招くタイムアウトを引き起こす。
- スナップショットの容量がメモリを圧迫する
  - アプリが落ちてしまう。

### どう解決したか

- 第一の案：Remote Query
  - test processが容量の小さいクエリ自身をアプリケーションに送信してクエリ評価をアプリケーションが行い、結果(これも容量が小さい)のみをtest processに返すことで送受信のオーバーヘッドを無くした。
  - 速度が20%上昇し、メモリ使用率を30%減少させた。
  - **でも満足できなかった。**
- 第二の案：Query Analysis
  - シンプルに集めるUI部品のデータを少なくして、スナップショットの容量を小さくする。
  - 速度が50%上昇し、メモリ使用率を35%減少させた。
  - **でももっといいアイデアがないかなと思った。**
- 第三の案：First Match
  - `var firstMatch: XCUIElement { get }`の登場。
  - 従来はスナップショット全域を検索していたが、最初にマッチしたときにクエリを終了するようにした。
  - **速度は数十分の一に、メモリ使用率はほぼ0になった。**
  - しかし、これを利用すると曖昧なクエリを検出できない。
    - 次のように、できるだけ曖昧さをなくすクエリの書き方が重要になってくる。

```swift
let button = app.buttons.firstMatch    // not a good idea
let button = app.buttons["Done"].firstMatch    // better
let button = app.navigationBars.buttons["Done"].firstMatch    // best
```

### `NSPredicate`の使用について

- `NSPredicate`を使用すると、Remote QueryもFirst MatchもQuery Analysisも使えない。
  - `Format String`か`NSExpression`に置き換える。
  - 置き換えられない状況があればAppleに連絡してくれ、とのこと。笑

## Activities, attachments, and screenshots

### Activities

```swift
XCTContext.runActivity(named: "Compose coffee message") { _ in
  // Compose and send a new message
  let composeView = writerApp.textViews["Compose message"]
  composeView.tap()
  composeView.typeText("Any good coffee places around McEnery? ☕ /cc @jane")
  writerApp.buttons["return"].tap()
  writerApp.buttons["send"].tap()
}
```

上記のように`XCTContext.runActivity(named name: String, block: (XCTActivity))`のブロックに一連の処理を渡すと、テストレポートの中でブロック内の処理をグルーピングして表示してくれる。

### Attachments

テストの内容を考察しやすいように、テスト結果を表す様々な種類のリッチなデータをテストレポートに添付できるようになった。
その代表例が次に紹介するScreenshotsである。

### Screenshots

#### `XCUIScreenshotsProviding`プロトコルの登場
- `XCUIElement`と`XCUIScreen`で`screenshot()`メソッドが使用可能に。
- ボタンならボタンのサイズ、ウインドウならウインドウのサイズにクリップされたスクリーンショットが取得可能に。
- テストを通過するとそれらのスクリーンショットは自動的に削除される。テストに失敗すると残る。
  - Schemeでこの設定は変更可能。

#### デモ

- Test DiamondをCtrl-ClickしてJump to Reportを選択するとテストレポートを見れる。
- 実際にスクリーンショットをキャプチャするには次のようにする。
  - このようにしてテストを実行したあとにテストレポートの「Gather Screenshots」の欄を見ると「目」のマークがついたスクリーンショットがある。
  - この「目」のマークをクリックしてもスクリーンショットは確認できるが、Assistant Editorを開いても確認することができる(拍手)。

```swift
XCTContext.runActivity(named: "Gather Screenshots") { activity in
  // Capture the full screen
  let mainScreen = XCUIScreen.main
  let fullScreenshot = mainScreen.screenshot()
  let fullScreenshotAttachment = XCTAttachment(screenshot: fullScreenshot)
  fullScreenshotAttachment.lifetime = .keepAlways    // テストが成功してもスクリーンショットが消えない
  activity.add(fullScreenshotAttachment)

  // Capture just the first cell
  let cell = readerApp.cells.element(boundBy: 0)
  let firstCellScreenshot = cell.screenshot()
  let firstCellScreenshotAttachment = XCTAttachment(screenshot: firstCellScreenshot)
  firstCellScreenshotAttachment.lifetime = .keepAlways
  activity.add(firstCellAttachment)
}
```

# おわりに

いかがでしたでしょうか。
`XCTest`を用いたテストは不完全な部分が多くて手を出している人も少ないような気がしますが、Xcode9からは大幅に強化されるみたいなので、これをきっかけにUI Testを導入してみてはいかがでしょうか。

最後に、記事の内容に誤りなどがございましたら、気軽に、優しく、コメントしていただけると嬉しいです。
それでは、最後までお読みいただき、ありがとうございました！