DeNA Testing Blog

Make Testing Fun, Smart, and Delighting End-Users

Xcode 11でのテスト周りの新機能を紹介します!

SWETグループ、iOS自動テスト領域チームの細沼(tobi462)です。

今回はWWDC19で発表された内容の中から、 Xcode 11におけるテストまわりの新機能について紹介します!

新しく追加された機能は大きく以下の3つです。

  • Test Plans(テストプラン)
    • テスト実行の設定(実行対象、言語・ロケール、他)を管理できる仕組み
  • Result Bundle
    • テストの成果物(ビルドログ、テストレポート、他)をまとめる仕組み
  • XCTest - XCTUnwrap()
    • XCTAssertNotNil + guard letに相当する関数

なお、メトリクスまわりも大きく進化していますが、本記事では割愛します。

関連するWWDC19のセッション

それぞれの機能について解説する前に、 関連するWWDC19のセッションを紹介したいと思います。 本記事で解説する自動テストに関するセッションに加え、 デバッグやメトリクスが解説されているセッションもあわせて紹介します。

なお、現在では日本語字幕にも対応しています。 どれも素晴らしいセッションとなっているので、 まだ見られていない方は是非ご覧ください。

Testing in Xcode(#413)

今回のWWDC19において、自動テストをテーマとした最も大きなセッションです。

セッションは自動テストのバランスを考える上で大切な「テストピラミッド」の紹介から始まり、 XCTestやカバレッジなどの基本的な利用方法についての説明を経て、 新機能である「Test Plans」や「Result Bundles」の解説がされます。

またセッションの最後では、 自分でCI環境を構築する方法についても解説され、 とても盛り沢山なセッションとなっています。

セッション中のデモがとても分かりやすく、 テストをまだ書いたことがない人にも是非見ていただきたい内容です。

Debugging in Xcode 11(#412)

Xcode 11で追加されたデバッグまわりの機能について、解説されたセッションです。

SwiftUIまわりのデバッグ機能についての紹介が中心となっていますが、 通信環境や端末温度といったデバイス状態について、 上書きしてエミュレートする機能なども紹介されています。

デバッグは自動テストと直接関係するわけではありませんが、 アプリを効率的に開発すするための必須テクニックとして、 iOSアプリ開発者であれば是非とも抑えておきたい機能です。

What's New in Xcode 11(#401)

Xcode 11の新機能について、解説されたセッションです。

自動テストとは直接関係ないセッションですが、 「Test Plan」や「デバイス状態の上書き」といったデバッグ機能について紹介されています。 Testing in Xcode(#413)Debugging in Xcode 11(#412) といったセッションとあわせて見るとより理解が深められる内容かと思います。

Xcode 11では「エディタ分割の強化」や 「Swift Package Managerのサポート」など、 テスト以外についても多くの機能が追加されています。

iOSアプリ開発者であれば必見のセッションといえます。

Improving Battery Life and Performance(#417)

アプリのメトリクス取得について解説されたセッションです。

メトリクス取得についての基本的な考え方から、 XCTestに追加されたAPI、 新しく追加されたMetricKitなどについて解説されています。

冒頭で記載したように、 本記事ではメトリクス取得については触れませんが、 興味のある方は是非このセッションをご覧ください。

Xcode 11におけるテスト周りの新機能

それではXcode 11で追加されたテスト周りの新機能について、次の順で解説していきたいと思います。

  • Test Plans(テストプラン)
  • Result Bundle
  • XCTest - XCTUnwrap()

Test Plans(テストプラン)

Test Plansはその名のとおり、 テスト実行における計画(テスト対象、コンフィギュレーション)を管理できる機能となっています。 WWDC19のセッションでは具体的な利用例として、 多言語(日本語、英語、など)のテストなどが紹介されていました。

従来はスキームを利用して実現していたものですが、 スキーム設定ではビルドの管理といった別の用途に利用されるため、 こうしたテスト実行の管理をするための仕組みとしては煩雑でした。

今回それらが「Test Plans」という形でスキームから独立し、 テスト実行の設定を直感的に分かりやすく管理できるようになりました。

まさに、「Test Plans(テスト計画)」という名に恥じない機能となっています。

Test Plansを導入する

既存プロジェクトにTest Plans導入するには、 既存のスキーム設定から変換する方法が簡単です。

変換はスキーム設定>Test>Infoタブにある「Convert to use Test Plans...」ボタンから行います。

ダイアログが表示されるので、スキーム設定から変換する場合は一番上を選択します。

任意のファイル名(今回は AllLanguage.xctestplan)で保存すると、Test Planに切り替わります。

Test Plansの設定

プロジェクトナビゲータに追加された.xctestplanを選択すると、 「Tests」と「Configurations」タブから構成される設定画面が表示されます。

「Tests」タブはスキーム設定画面におけるテスト一覧と同じような見た目をしており、 同様に「実行するテスト対象」を選択する画面となっています。

「Configurations」タブは、 テスト実行時における設定(以降「コンフィギュレーション」と表記)を設定する画面となっており、 前述した言語設定(日本語、英語など)にくわえカバレッジ取得の有無なども設定可能です。

コンフィギュレーションは1つの.xctestplanで複数持てるようになっており、 左側のサイドバーにその一覧が表示されるようになっています。

「Shared Settings」と表示されているのは共通設定、 その下に表示されたものが個別のコンフィギュレーション(スクリーンショット中では「Configuration 1」)となっており、 個別のコンフィギュレーションは「Shared Settings」の設定値を継承する仕組みとなっています。

複数の設定を用意して実行する

例として「日本語」と「英語」の2つのコンフィギュレーションを用意してテスト実行する例を紹介します。

まず左下の「+」ボタンから「Japanese」と「English」というコンフィギュレーションを作成します。 そして、それぞれの「Application Language」の項目を「Japanese」と「English」に設定します。

ここまで設定しておくと、 テストナビゲータのコンテキストメニューから実行するコンフィギュレーションを選べるようになります。 ここでは「All Configurations」を選択して、 すべてのコンフィギュレーションでテストを実行してみます。

レポートナビゲータでテストの実行結果を確認すると、 「Japanese」と「English」の2つのコンフィギュレーションで実行されていることが分かります。

当然ながら各コンフィギュレーションごとにテスト結果がわかるようになっており、 以下のアイコンからコンフィギュレーションで絞り込むことも出来ます。

コマンドラインから利用する

xcodebuildコマンドにもTest Plans用のオプションが追加されたので、ここで紹介したいと思います。

プロジェクトに用意されたTest Plansの一覧を表示するには-showTestPlansオプションが利用できます。

$ xcodebuild \
    -project ./TestPlanSample.xcodeproj \
    -scheme 'TestPlanSample' -showTestPlans

### 出力内容:
Test plans associated with the scheme "TestPlanSample":
        AllLanguage
        UITestJapanese

テスト実行時に利用するTest Plansを指定する場合は-testPlanを利用します。

$ xcodebuild test \
    -project ./TestPlanSample.xcodeproj \
    -scheme 'TestPlanSample' \
    -destination 'platform=iOS Simulator,name=iPhone Xs' \
#    -testPlan 'AllLanguage' # テストプラン「AllLanguage」を実行

なお、-testPlanを指定しないで実行した場合はスキーム設定でデフォルトになっているものが利用されます。

ちなみにデフォルトではすべてのコンフィギュレーションが実行対象となりますが、 次のオプションを利用することで限定することもできます。

  • -only-test-configuration:指定したものだけを実行
  • -skip-test-configuration:指定したものを除外して実行
$ xcodebuild test \
...
    -testPlan 'AllLanguage' \
#    -only-test-configuration 'English' # コンフィギュレーション「English」のみを実行

Result Bundles

Result Bundles(.xcresult)は、 今まで独立していた以下のようなテスト結果を1つにパッケージするものです。

  • ビルドログ
  • テストレポート
  • コードカバレッジ
  • テスト添付ファイルなど

またxcresulttoolというコマンドも追加され、 コマンドラインからも.xcresultファイルから情報を取得でき、 JSONスキーマで仕様が明確化されたJSONでの取得も可能になっています。

こうした変更により、テスト結果リソースの扱いがシンプルに行えるようになりました。

どのように変化したか?

Xcode 10.2までは以下のスクリーンショットのように、 それぞれの結果リソースが独立したファイルとして出力されていました。

それがXcode 11からは以下のキャプチャのように、 1つの.xcresultファイルとして集約されるようになりました。

単に集約されただけではなくデータサイズについても考慮されており、 WWDC19のセションの発表によると、 これまでに比べて「最大で4倍小さいサイズ」のフォーマットとなっているようです。

この.xcresultファイルはXcodeで開いて確認できるのに加え、 前述したようにxcresulttoolという専用のコマンドを利用して、 中身の情報を取得できるようになっています。

コマンドラインで出力先を指定する

xcodebuildコマンドで-resultBundlePathオプションを利用すると、 指定したパスに.xcresultファイルを出力することが出来ます。

$ xcodebuild test -project UnitTest.xcodeproj \
    -scheme UnitTest \
    -destination 'platform=iOS Simulator,name=iPhone Xs' \
    -resultBundlePath test_output/ResultBundle.xcresult

なお、このオプションを指定しなかった場合、 プロジェクトで設定されたDerivedDataフォルダ以下に出力されます。

DerivedData/{schemeName}/Logs/Test/Test-{schemeName}-{buildNo}.xcresult

Xcodeで開いて中身を確認する

Finderからダブルクリックするか、 「Xcodeメニュー>File>Open」から.xcresultファイルを選択して開くことが出来ます。

すると以下のキャプチャのように、テスト結果をXcode上から閲覧できます。

xcresulttoolの使い方

前述したようにxcresulttoolを利用すると、中身のデータを取得することが出来ます。

以下は、JSONフォーマットとして情報を取得する例です。

$ xcrun xcresulttool get \
  --path ResultBundle.xcresult \
  --format json

出力されるJSONを抜粋すると以下のようになっています。

  "issues" : {
    "_type" : {
      "_name" : "ResultIssueSummaries"
    },
    "testFailureSummaries" : {
      "_type" : {
        "_name" : "Array"
      },
      "_values" : [
        {
          "_type" : {
            "_name" : "TestFailureIssueSummary",
            "_supertype" : {
              "_name" : "IssueSummary"
            }
          },
...
          "issueType" : {
            "_type" : {
              "_name" : "String"
            },
            "_value" : "Uncategorized"
          },
          "message" : {
            "_type" : {
              "_name" : "String"
            },
            "_value" : "XCTAssertTrue failed - \"hello\" is not empty"
          },
          "producingTarget" : {
            "_type" : {
              "_name" : "String"
            },
            "_value" : "UnitTestTests"
          },
          "testCaseName" : {
            "_type" : {
              "_name" : "String"
            },
            "_value" : "OriginalAssertionGoodTests.testAssertEmpty()"
          }
...

各項目ついての説明は割愛しますが、 かなり詳細にデータを取得できるのが分かります。

なお、JSONスキーマの取得は以下のコマンドで行えます。

$ xcrun xcresulttool formatDescription get

同様に出力結果を抜粋すると以下のようになります。

Name: Xcode Result Types
Version: 3.21
Signature: PVlziQ9KewI=
Types:
  - ActionAbstractTestSummary
    * Kind: object
    * Properties:
      + name: String?
  - ActionDeviceRecord
    * Kind: object
    * Properties:
      + name: String
      + isConcreteDevice: Bool
      + operatingSystemVersion: String
      + operatingSystemVersionWithBuildNumber: String
      + nativeArchitecture: String
      + modelName: String
      + modelCode: String
...

その他コマンドの詳細についてはman xcresulttoolから確認できます。

コードカバレッジ結果の取得

以前からxccovコマンドによってカバレッジレポートの中身を参照できました。 ある意味当然といえますが、 今回から.xcresultファイル形式もサポートされました。

.xcresultファイルからカバレッジ確認する際は、以下のコマンドで行えます。

$ xcrun xccov view --report ResultBundle.xcresult

参考まで出力結果の画面キャプチャを貼っておきます。

XCTest - XCTUnwrap()

本記事の最後として、 XCTestに追加されたXCTUnwrap()というAPIについて解説します。

XCTUnwrap()は、 オプショナル型に対してXCTAssertNotNilのアサーションと、 アンラップ処理を同時に行うAPIです。

具体的なコード例を挙げると、 これまでは以下のようなコードを書く必要がありました。

func testExample() {
    
    let string: String? = "Hello"
    
    // `nil`でないことを検証+アンラップ
    guard let s = string else { XCTAssertNotNil(string); return }
    
    // 検証
    XCTAssertEqual(s, "Hello")
}

それがXCTUnwrap()を利用すると以下のように書けるようになります。

func testExample() throws { // `throws`キーワードが必要な点に注意
    
    let string: String? = "Hello"
    
    // `nil`でないことを検証+アンラップ
    let s = try XCTUnwrap(string)
    
    // 検証
    XCTAssertEqual(s, "Hello")
}

なお、guard letではなく強制アンラップ(!)を利用すれば、 XCTUnwrap()を利用せずともシンプルに書けるのではないか、 と感じた方もいるかもしれません。

強制アンラップは記述こそ簡単ですが、 アンラップの失敗によりテストケースが失敗した場合、 「どの箇所で失敗したのか」という情報が失われ原因調査にコストがかかるという欠点があります。

なので、オプショナル型の中身を取り出して検証したい場合は、 今回追加されたXCTUnwrap()を利用していくとよいでしょう。

なお、T型とOptional<T>型はXCTAssertEqual()などの関数で比較可能なので、 中身を取り出す必要がない場合には以下のように記述できます。

let string: String? = nil
XCTAssertEqual(string, "Hello")

おわりに

本記事では、 WWDC19で発表されたXcode 11における自動テスト周りの変更点として、 「Test Plans」「Result Bundle」「XCTUnwrap()」について紹介しました。

今年のWWDC19では「SwiftUI」や「Catalyst」が大きく注目を集めていましたが、 開発における基盤を支える機能もしっかり進化していたことが分かります。 特に本記事で取り上げたような自動テスト周りの機能は、 ここ数年でかなり成熟してきたようにも感じます。

本記事で主要な機能については解説しましたが、 より詳細なテクニック・組み合わせ方法については、 まだ十分にナレッジを得られていない部分もあると感じています。

より多くの方がiOSアプリ開発コミュニティに対して、 様々な知見を公開してくださると嬉しい限りです。

仲間を募集中

お約束ではありますが、 SWETチームでは自動テストやCI/CDを活用する仕事に興味を持った方を募集しています。

以下のエントリフォームから応募できるほか、 私や他のSWETメンバーに声をかけて頂くかたちでも大丈夫です。

是非ともお気軽にご連絡ください!