Xcode 11 の Test Plan を、試しながらゆるく解説

この記事では WWDC 2019 の 「[Testing in Xcode](https://developer.apple.com/videos/play/wwdc2019/413/)」 をベースにして、Test Plan 
 について調べたことや試してみたことを書こうと思います。すぐに Test Plan を試せるプロジェクトは↓こちら。
https://github.com/akatsuki174/TestPlanSample

# Test Plan の存在意義

例えばローカライズ対応をしているアプリで、開発時のデフォルト言語ではテストがパスしても、別の言語で実行した時に UI 崩れが発生するかもしれない。複数の条件（言語、テストの実行順、サニタイザー、引数や環境変数）を跨いで常にテストを実行することは不具合の発見率を高めることができる。

今までの Xcode でもスキームエディタを使うことによって様々な設定をすることが可能だったが、実行は1回しかできない。一方 Test Plan では設定を変えてテストを何回も実行できるところがメリットになる。

# Test Plan でできること

* 設定を変えてテストを複数回実行
* 全てのパターンを1箇所に定義
* 設定を複数のスキームで共有
* CI サーバや Xcode Server で使う（xcodebuild にも対応している）
* 既存プロジェクトに導入

# xctestplan ファイルの作り方

Test Plan を使うために xctestplan ファイルというテスト設定ファイルを作成する。

①Edit Scheme を選択
<img width="372" alt="スクリーンショット 2019-12-08 21.35.51.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/dd689522-442e-eb5f-ce0c-b1acdbea3034.png">

②Test タブを選択し、下の `Convert to use Test Plans` をクリック
<img width="896" alt="スクリーンショット 2019-12-08 21.37.40.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/62265fe7-e158-61f6-91ce-388f13682d53.png">

③自分に合った変換方法を選択

既存の設定を引き継いで Test Plan を作る、空の Test Plan を作る、すでにある Test Plan を使うの3種類の方法を取ることができる。
<img width="898" alt="スクリーンショット 2019-12-08 21.54.42.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/ee809a15-e339-0c3e-144b-e7df83a30704.png">

ちなみにここからでも Test Plan ファイルの新規作成や編集ができるっぽい。
![スクリーンショット 2019-12-09 23.12.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/65772769-e7c4-d976-e05b-1eac2361a5df.png)

# xctestplan ファイルの中身 - Tests タブ

Test タブはこの通り。存在しているテストターゲットが一覧になっている。Option をクリックすると、並列でテストを走らせるか否かなどの設定を行うことできる。

特定のターゲットのテストを無効にしたい場合は、Enabled のチェックを外す。
<img width="359" alt="スクリーンショット 2019-12-08 22.35.29.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/68ca8181-ae3f-df7a-2623-d5807481152f.png">

# xctestplan ファイルの中身 - Configurations タブ

実行の方法を定義している部分。設定できる項目の大区分は以下の通り。

* 引数
* ローカライズ
* UI Test
* アタッチメント
* テスト実行
* コードカバレッジ
* ランタイムサニタイザー
* ランタイムAPIチェック
* メモリ管理

Share Settings では毎回のテストに共通するオプションを設定する。太字になっているところは、カスタム値を設定しているという意味。
![スクリーンショット 2019-12-09 23.20.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/4bbcd2ce-0791-4f32-6d8c-17d4c8216dbf.png)

条件を変えてテストを実行したい場合は新しく Config ファイルを作成してカスタマイズする。詳細は次の例の中にて。

# 使ってみる

## 2ヶ国語の翻訳表示テスト

日本語と英語での翻訳表示がちゃんとできているか確認する UI テストの例。ただ Test Plan を試したいだけなのでテストの内容がアレなのはご容赦。
Label に表示されている文字列が正しいかをチェックする。

```swift
class ViewController: UIViewController {

    @IBOutlet weak var greetingLabel: UILabel!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        greetingLabel.text = NSLocalizedString("Top_title_label", comment: "")
    }
}
```

```swift
class TestPlanSampleUITests: XCTestCase {
    
    var app: XCUIApplication!

    override func setUp() {
        continueAfterFailure = false
        app = XCUIApplication()
        app.launch()
    }

    func testLabelLocalization() {
        XCTAssertEqual(app.staticTexts["greetingLabel"].label, localizedString(key: "Top_title_label"))
    }
    
    private var currentLanguage: (langCode: String, localeCode: String)? {
        let currentLocale = Locale(identifier: Locale.preferredLanguages.first!)
        guard let langCode = currentLocale.languageCode else {
            return nil
        }
        var localeCode = langCode
        if let scriptCode = currentLocale.scriptCode {
            localeCode = "\(langCode)-\(scriptCode)"
        } else if let regionCode = currentLocale.regionCode {
            localeCode = "\(langCode)-\(regionCode)"
        }
        return (langCode, localeCode)
    }

    private func localizedString(key:String) -> String {
        let localizationBundle = Bundle(path: Bundle(for: TestPlanSampleUITests.self).path(forResource: currentLanguage?.langCode, ofType: "lproj") ?? "")
        let result = NSLocalizedString(key, bundle:localizationBundle!, comment: "")        
        return result
    }
}
```

準備は各言語用の Config ファイルを作るだけ。Config の追加は見慣れたこの＋ボタンをクリックすればOK。
![スクリーンショット 2019-12-13 20.53.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/0a618654-0530-53fe-ef51-2363e3ca2b57.png)

設定を変えるのは Localization の部分。

↓日本語の Config
![スクリーンショット 2019-12-13 20.51.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/a93b1c66-53e2-a526-bc42-36719116d18a.png)

↓英語の Config
![スクリーンショット 2019-12-13 20.57.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/633fe8da-44e7-c1d2-c4b3-9008729579ad.png)

これを実行すると、このように2回連続でテストが走る。
![testplan-lang.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/6a4dfc1f-ac04-a709-ef82-a0450690a9e2.gif)

もし1言語だけテストを回したいなら、テストファイルで Option を押しながらひし形をクリックする。すると使用する Config の選択肢が出てくるので、使いたい方を選ぶ。
![スクリーンショット 2019-12-13 21.13.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/492ec636-5c49-cd1c-2ac0-5cbd35f0fe1d.png)

ではここでわざと日本語のテストが失敗するようにしてみる。

```swift
override func viewDidLoad() {
    super.viewDidLoad()
    var text = NSLocalizedString("Top_title_label", comment: "")
    let lang = Locale.preferredLanguages.first
    if lang == "ja" {
        text += "hoge"
    }
    greetingLabel.text = text
}
```

想定通り失敗する。
![スクリーンショット 2019-12-13 23.55.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/a7eac25d-f0e3-748f-d7a7-159082e7417f.png)

どのメソッドで、どの Config の時に失敗したのかは、レポートナビゲータからチェックできる。
![スクリーンショット 2019-12-14 0.38.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/0ad5a4ad-d9fd-bf73-18a2-870ecb7c54b0.png)
Allタブでは全ての結果が、Passed タブではテストが通ったメソッドが、Failed タブではテストが通らなかったメソッドが、Mixed では Config によって結果が異なるメソッドが表示される。
![スクリーンショット 2019-12-14 0.04.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/9ea5b60e-23ef-e3c4-d632-658bfaddb91e.png)
特定の Config の結果だけ見たいときはこのボタンをクリックして選択する。
![スクリーンショット 2019-12-14 0.40.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100736/23ad3bff-66a1-097b-6067-b7f71b87d4f7.png)

# Test Plan の活用例

## 異なるサニタイザーを組み合わせて使う

サニタイザーは Xcode に内蔵されているツールで、再現しにくいバグを特定してくれる。例えば ASan と UBSan を組み合わせて使うといったことができる。

## Language, Location, Locale 別に Config を用意する

各国の言語で UI テストを実行しつつ、Shared Settings でスクショを撮る設定をする。Xcode 11 では成功したテストでも全てのスクショを保存できる。

## 異なる種類の項目を組み合わせる

* メモリの安全性を重視：Address Sanitizer + Zombie Objects
* 並列処理を重視：Thread Sanitizer + Undefined Behavior Sanitizer + Random Order
* 追加診断：ENABLE_LOGGING=1（カスタム環境変数） + Keep Attachments（テストが成功してもAttachmentsを保持）