Xcode 9.0 Beta 1におけるXCTest.frameworkの変更点


昨年これといった変更がなかった[XCTest.framework](https://developer.apple.com/documentation/xctest)ですが、今年は色々と追加や変更があり、WWDC2017でも「[What's New in Testing](https://developer.apple.com/videos/play/wwdc2017/409/)」というセッションが設けられました。

本記事ではXCTest.framework in Xcode 9.0 Beta 1の追加や変更を取り上げます。

# XCTest.frameworkの歴史

2013年: XCTest.framework誕生
2014年: 非同期処理のテストやパフォーマンスのテストができるようになった
2015年: UIテストができるようになった
2016年: 沈黙
2017年: APIが増えたり型が変わったりスクショが取れるようになったりした

# XCTest.frameworkの主な変更点

__deprecatedはありません。__

細かい変更点:

* あちこちで `Int` 型引数が `UInt` 型に変わった。 
* 一部で引数から `@escaping` が取れた。
* その他、引数の型が色々変わった。
  * `func measureMetrics` の引数の1つが `String` から `XCTPerformanceMetric` ( `init(String)` を持つ)に変わるなど

今回の目玉:

* 非同期テスト用のAPIの設計が見直された
* teardownブロックが導入された
* 名前ありアクティビティが導入された
* マルチAppテストができるようになった
* UI Elementの仲間が増えたりパフォーマンスを見直したりした
* スクショが撮れるようになった
* テスト中にできたデータ(スクショ含)を保存できるようになった

## 非同期API

[Testing Asynchronous Operations with Expectations | Apple Developer Documentation](https://developer.apple.com/documentation/xctest/asynchronous_tests_and_expectations/testing_asynchronous_operations_with_expectations)

### (復習)従来の非同期処理テスト方法

1. `func expectation(description: String)` を呼ぶ。
2. 処理終了時に `func fulfill()` を呼ぶ。
3. `func waitForExpectations(timeout: TimeInterval, handler: XCWaitCompletionHandler? = nil)` で待つ。

### 問題点

* class XCTestCaseに直接実装されていた。
* タイムアウト時は問答無用で常に失敗扱いにされた。慈悲はない。
* waitForExpectationsがタイムアウトとハンドラしか取らないせいで何を待機しているのか非明示的だった。

### 今回の改善点の概要

Xcode8.3:

* 実装がXCTestCaseから独立した
* 何を待機するのか具体的に書くwaitメソッドができた
* `XCTestExpectation` の種類が豊富になった
* エラー時の処理を書けるようになった
* タイムアウト時にテスト成功にするか失敗にするか、開発者が決められるようになった

Xcode9.0 Beta 1:

* 非同期処理時のエラーを表す `XCTestError` ができて、waitForExpectationsのハンドラ( `(Error?) -> Void` )で処理を具体的に行えるようになった

### 実装の独立

中身の話なのでAPI自体に変更はない。が、waitForExpectationsを見に行くと様子が変わっているのがわかる。

### `XCTestExpectation`

これまでは `func expectation(description: String)` で作っていたが、イニシャライザで作ることもできるようになった。ただし、イニシャライザで作った場合は登録まではしてくれないのでwaitForExpectationsが認識しない。

`NSNotification` を待つ `XCTNSNotificationExpectation` など種類が増えた。

### 今使えるwaitメソッド

結果的に待機に使うメソッドは3種類存在する。

* (XCTestCase) waitForExpectations
* (XCTestCase) wait
* (XCTWaiter) wait

#### (XCTestCase) waitForExpectations

`func waitForExpectations(timeout: TimeInterval, handler: XCWaitCompletionHandler? = nil)` 自体に変更はない。
`XCWaitCompletionHandler` が `(Error?) -> Void` のtypealiasであることにも変更はない。

が、この `(Error?)` が `XCTestError` を取ってくるようになった。まるでエラー全般をとってくるような名前をしているが、非同期待機中のエラーのみを扱う。 `XCTestError` 自身はstructであり、エラーの種類はenum `XCTestError.Code` が列挙している。

```swift
waitForExpectations(timeout: 10) { error in
    guard let error = error as? XCTestError else {
        return
    }
    switch error.code {
    case .failureWhileWaiting:
        print("failure")
    case .timeoutWhileWaiting:
        print("timeout")
    }
}
```

ハンドラを書いたところで最終的にテストが失敗扱いになるのは従来から変わっていない。

#### (XCTestCase) wait

`func wait(for: [XCTestExpectation], timeout: TimeInterval)` など。引数で `XCTestExpectation` を与えないといけないので、こいつが何を待つのかわかりやすくなった。

待機中にタイムアウト等のエラーが起きた場合は、このメソッドを持つXCTestCase(が実装している `XCTWaiterDelegate` )のデリゲートのメソッドが呼ばれる。
`func waiter(XCTWaiter, didTimeoutWithUnfulfilledExpectations: [XCTestExpectation])` とか。

これを使うとタイムアウトでもエラーにならなくなるので、エラー扱いにしたい場合は該当メソッドをoverrideして自力でfailする。

#### (XCTWaiter) wait

XCTWaiter:
XCTestCase版waitよりもできることが多い子。XCTestCase自身以外をデリゲートに使いたい場合は `init(delegate: XCTWaiterDelegate?)` でデリゲートを指定する。省略した場合はXCTestCaseのメソッドが呼ばれる。

XCTWaiterのwaitは `func wait(for expectations: [XCTestExpectation], 
  timeout seconds: TimeInterval) -> XCTWaiter.Result` など。enum XCTWaiter.Resultで結果が取れるので、デリゲートメソッドで処理するだけでなく戻り値で分岐することもできる。

```swift
let result = XCTWaiter().wait(for: [expectation], timeout: 10)
switch result {
case .completed:
    break
case .timedOut:
    break
default:
    break
}
```

## teardownブロック

[Understanding Setup and Teardown for Test Methods | Apple Developer Documentation](https://developer.apple.com/documentation/xctest/xctestcase/understanding_setup_and_teardown_for_test_methods)

これまでにもXCTestの `func tearDown()` やXCTestCaseの `class func tearDown()` がいたが、今回 `func addTeardownBlock(_ block: @escaping () -> Void)` が増えた。つまり動的に/個別にteardownをつけられるようになった。

```swift
func testExample() {
    print("testExample")
    addTeardownBlock {
        print("teardown in testExample()")
    }
}
```

実行順は

1. `func setUp()`
2. `func testExample()`
3. addしたteardownブロックたち
4. `func tearDown()`

である。このteardownブロックはLIFOで管理されているため、setUpとtextExampleで積んだteardownブロックは逆順に処理される。teardownブロックやtearDownのステップに突入してから積もうとすると実行時にエラーになるので、teardown中にaddTeardownBlockしてはいけない。

実行順の図が[Understanding Setup and Teardown for Test Methods | Apple Developer Documentation](https://developer.apple.com/documentation/xctest/xctestcase/understanding_setup_and_teardown_for_test_methods)に乗っている。

積むのはどのスレッドからでも可能だが実行はメインスレッドがやる。
teardownに限らずテストメソッドはrethrow禁止。自分でcatchまでやる。

## 名前ありアクティビティ

[Grouping Tests into Substeps with Activities | Apple Developer Documentation](https://developer.apple.com/documentation/xctest/activities_and_attachments/grouping_tests_into_substeps_with_activities)

### これまでの問題点

UIテストをやるとログにUI操作が1つ1つ記録され、それが長くて読みにくかった

### 今回の変更点

`class func runActivity(named: String, block: (XCTActivity) -> ())` を持つclass `XCTContext` ができた。与えた名前はログで使われ、さらにblock内のログ出力が畳まれて読みやすくなる。
runActivityは入れ子にすることができるので、どんどんログを畳むことができる。

共通操作を `(XCTActivity) -> ()` に切り分けてrunActivityで呼び出すなど。

```swift
func testExample() {
    XCTContext.runActivity(named: "stepA") { a in
        XCTContext.runActivity(named: "stepA-1") { a_1 in
        }
        XCTContext.runActivity(named: "stepA-2", block: hogeActivity)
    }
}

func hogeActivity(activity: XCTActivity) {
}
```

## マルチAppテスト

UIテスト中に複数のアプリを起動し、両方を操作してテストすることができるようになった。

### アプリのイニシャライザ

アプリを扱うのはclass `XCUIApplication` であり、イニシャライザが3つ用意されている。

`init()` :
target applicationで指定されているアプリが呼ばれる。UIテストありでプロジェクトを作成した時にできるサンプルのfunc setUp()内にいるのはこれ。

`init(bundleIdentifier: String)`:
"com.example.HogeApp"と書く。

`init(url: URL)`:
macOS専用。
/Applications/Slack.appって書いたら動いた。

### アプリ起動

元からいる `func launch()` に加えて `func activate()` が増えた。
同期処理であること、起動中にエラーがあった場合はテスト失敗の扱いになることは共通。
違う点はすでにアプリが起動している時に呼んだ場合の挙動で、launch()はアプリを再起動させるがactivate()は停止させない点にある。なので、別のアプリを操作した後にresumeさせるのに使える。

```swift
let vim = XCUIApplication(url: URL(fileURLWithPath: "/Applications/MacVim.app"))
let emacs = XCUIApplication(url: URL(fileURLWithPath: "/Applications/Emacs.app"))
vim.launch()     // 起動
emacs.launch()   // 起動
vim.launch()     // 再起動
emacs.activate() // 手前に来るだけ
```

### アプリの状態の確認

`XCUIApplication` の `var state: XCUIApplication.State` でアプリの状態が取れる。
`XCUIApplication.State` はenumで、 `case runningForeground` や `case notRunning` などがいる。

## UI Elements

### 変更点

* ステータスメニューを扱えるようになった
* エレメントが `exists` になるまで待てるようになった
* `var firstMatch: XCUIElement` で速くエレメントを取れるようになった

### ステータスメニュー

macの右上にいるアレ。
[macOS Sierra: メニューバーの内容](https://support.apple.com/kb/PH25077?locale=ja_JP&viewlocale=ja_JP)

いつものenum `XCUIElement.Type` に `case statusItem` が増えている。

### `exists` になるまで待機

UIElementが現れる前にタップ操作などをするとエラーになってしまったが、 `func waitForExistence(timeout: TimeInterval)` で安全に待つことができるようになった。
`var exists: Bool` がtrueになるまで待ち、タイムアウトした場合はテスト失敗とする。

```swift
let application = XCUIApplication()
application.launch()
let hogeButton = application.buttons["hoge"]
hogeButton.waitForExistence(timeout: 10)
hogeButton.tap()
```

### firstMatch

欲しい `XCUIElement` を取るのは時間のかかることだったが、 `var firstMatch: XCUIElement` により最初の1つを発見した段階で早めに切り上げられるようになった。
`buttons.firstMatch` よりは `navigationBars.buttons["Done"].firstMatch` のように具体的な方が望ましいとのこと。

## スクリーンショットを撮る

protocol `XCUIScreenshotProviding` というスクショメソッドを与えるプロトコルができた。このプロトコルは `func screenshot() -> XCUIScreenshot` のみを持つ。そしてこのプロトコルを実装しているのは

* `XCUIScreen`
* `XCUIElement`

の2つである。

### `XCUIScreen` からスクショを撮る

`XCUIScreen` には　`class var main: XCUIScreen` と `class var screens: [XCUIScreen]` があり、ここからscreenshot()を呼ぶことができる。

```swift
XCUIScreen.main.screenshot()
XCUIScreen.screens.map { $0.screenshot() }
```

mainは名前の通りメインスクリーンを得ることができる。
screensはデバイスにディスプレイが複数繋がっている時用。0番目がメインスクリーンになる。

(iPhone7シミュレータでscreensをやったらなぜか2枚取れたし、謎の1番目をスクショしようとしたらCannot capture a non-main screenって言われた。。)

### `XCUIElement` からスクショを撮る

XCUIElementでwindowsを探す。 `windows.firstMatch.screenshot()` など。
(windows以外でやろうとしたら音もなくエラーになった。)

### `XCUIScreenshot` が撮れたら

クイックルックで確認:
ブレイクポイントで止め、XCUIScreenshotが入っているモノの👁アイコンから確認する。ここからPreview.appを開けば保存もできる。

型の変更:
`XCUIScreenshot` は `var image: UIImage` と `var pngRepresentation: Data` を持っていて変換ができる。

保存:
次に紹介する `XCTAttachment` を使う。

## テスト中に得たデータの保存

[Adding Attachments to Tests and Activities | Apple Developer Documentation](https://developer.apple.com/documentation/xctest/activities_and_attachments/adding_attachments_to_tests_and_activities)

`XCTAttachment` を用いることでデータを保存できるようになった。

方法:

1. 保存したいデータを使って `XCTAttachment` をイニシャライズする
2. `XCTActivity` のメソッド `func add(XCTAttachment)` でaddする(XCTestCaseが実装済み)
3. 必要に応じてデータ保持期間を指定
4. テストしたらログから確認したり保存したりする

```swift
let attachment = XCTAttachment(screenshot: screenshot)
attachment.lifetime = .keepAlways  // 成功後もデータを保持
add(attachment)
```

### イニシャライザ

以下のものからイニシャライズできる。つまり以下の型で表されるデータを保存できる。

* `Data`
* `URL` : ファイルやフォルダ。フォルダをzipにすることもできる
* `UIImage` : 画質指定も可能
* `XCUIScreenshot`
* `Any` : plistのこと
* `NSSecureCoding`
* `String` : .text 

### 保持期間

何も指定しない場合、テスト成功時に消されてしまう。成功したい時でも保持して確認したい場合はその旨を指定する。

* 各 `XCTAttachment` の `var lifetime: XCTAttachment.Lifetime` に与える
* テストschemeで一括指定する

# まとめ

既存APIを残したまま新機能が増えた。