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


この記事では WWDC 2019 の 「Testing in Xcode」 をベースにして、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 を選択

②Test タブを選択し、下の Convert to use Test Plans をクリック

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

既存の設定を引き継いで Test Plan を作る、空の Test Plan を作る、すでにある Test Plan を使うの3種類の方法を取ることができる。

ちなみにここからでも Test Plan ファイルの新規作成や編集ができるっぽい。

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

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

特定のターゲットのテストを無効にしたい場合は、Enabled のチェックを外す。

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

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

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

Share Settings では毎回のテストに共通するオプションを設定する。太字になっているところは、カスタム値を設定しているという意味。

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

使ってみる

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

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

class ViewController: UIViewController {

    @IBOutlet weak var greetingLabel: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()
        greetingLabel.text = NSLocalizedString("Top_title_label", comment: "")
    }
}
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。

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

↓日本語の Config

↓英語の Config

これを実行すると、このように2回連続でテストが走る。

もし1言語だけテストを回したいなら、テストファイルで Option を押しながらひし形をクリックする。すると使用する Config の選択肢が出てくるので、使いたい方を選ぶ。

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

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
}

想定通り失敗する。

どのメソッドで、どの Config の時に失敗したのかは、レポートナビゲータからチェックできる。

Allタブでは全ての結果が、Passed タブではテストが通ったメソッドが、Failed タブではテストが通らなかったメソッドが、Mixed では Config によって結果が異なるメソッドが表示される。

特定の Config の結果だけ見たいときはこのボタンをクリックして選択する。

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を保持)