DeNA Testing Blog

Make Testing Fun, Smart, and Delighting End-Users

SWET視点でピックアップしたテストに関するWWDC 2020まとめ

お久しぶりです。 SWETグループの平田(@tarappo)と片山(@kariad_uu)です。

本稿では、先日オンラインでおこなわれたWWDC2020の中から、SWET視点で気になった次の2点について紹介したいと思います。

  • The suite life of testing
  • Instruments周り

これら以外にもテスト周りの追加としては待望の「Xcodeを用いたStoreKitのテスト」がありますが、本記事では割愛します。

本記事でアップしているXcodeの画像はXcode11.6でとった画像となります。 Xcode12では見た目が変わっている可能性があります。

The suite life of testing

まず、WWDC2020で公開された「The suite life of testing」について平田が紹介していきます。

WWDC2020では「The suite life of testing」として次の5つの動画が公開されています。

  • Handle interruptions and alerts in UI tests
  • Get your test results faster
  • XCTSkip your tests
  • Triage test failures with XCTIssue
  • Write tests to fail

これらの動画での説明はそれぞれがある程度関係をしていて、大きくまとめると「自動テストが失敗したときの対応」についての話がメインといえます。

そこで、ここでは1つ1つの動画の中身について説明するのではなく、これらの動画を簡潔にまとめて説明をします。 (なお「Handle interruptions and alerts in UI tests」についてはUIテスト時の割り込みやアラートをどのように対応するかの話なので今回は除外します)

自動テストは常に成功するわけでなく失敗もします。 この自動テストの失敗時の調査は、テストコードを維持し続けるために大事なことです。 しかし、調査にコストがかかりすぎてしまうのは問題です。

このコストがかかりすぎないように、動画内でも説明されているものとして、次の2つの観点で説明をしていきます。

  • フィードバックを早くする
  • フィードバックからの情報取得

フィードバックを早くする

自動テストからのフィードバックは早いことが望ましいです。 たとえば、CI/CDサービスで自動テストを動かしていて、いつまでたっても終わらないといったことが起きてしまうと結果を待ちきれなくなってしまいます。

その結果、自動テストを実行しないといったことにもつながってしまう恐れがあります。

次をおこなうことでフィードバックを早くできます。

  • テストの実行時間の制御
  • テストのスキップ
  • 並列実行

これらについて次に説明をしていきます。

テストの実行時間の制御

必要な理由

テストの実行時間がのびてしまう原因の1つとして、自動テストが想定の時間以上かかってしまうということがあります。

たとえば、テストのハングにより自動テストが終わらなかったとします。

その場合、CI/CDサービスの制限時間や実行している環境(デバイスファームなど)の制限時間により、テストが無理やり終了させられてしまうことになります。

そうすると、テストが無事に終了していないため自動テストの結果を得ることも出来ずに、なにが問題だったのかがわからなくなってしまいます。

Xcode11.4でテストの実行時間を制御できるようになりました。

この機能を利用することで、特定の時間以上がかかった場合にそのテストを失敗扱いにできます。

これにより、想定外の挙動をしてテストが終了せずに実行時間が長くなってしまうといったことを止めることができます。

制限時間の設定

本機能はTestPlansで下記画像にあるように「Test Timeouts」をONにすることで利用できるようになります。

f:id:swet-blog:20200813095532p:plain

テストの制限時間はデフォルトで10分が指定されています。 この値は、複数の方法で変更できます。

その際の優先度は次のとおりです。

  1. テストコード側で利用するexecutionTimeAllowance API
  2. テスト実行時に指定するxcodebuildのtimeallowanceオプション
  3. TestPlansの設定
  4. デフォルトの時間(10分)

これらの設定方法のうち、Xcode12から3にあるTestPlansからも値を設定できるようになりました。

これらの指定する値は、秒数で指定できますが注意が必要です。 たとえば、60秒未満を指定した場合は1分に切り上げられます。 100秒であれば、2分に切り上げられます。

この指定した時間以上に実行時間がかかった場合、Xcode上では次のようなエラーが表示されます。

f:id:swet-blog:20200813095427p:plain

この画像の例では制限時間を1分と指定していて、テストの実行時間がその時間を過ぎています。

なお、エラーが出たときの情報の詳細はXcode12からより詳細に出るようになったので、後述します。

テストのスキップ

必要な理由

自動テストを実装している中で、実行する環境に依存するようなテストを実装することもあります。

たとえば、ユニバーサルアプリでiPadのみで動くテストとか、特定のOSバージョン以上で動作するテストなどです。

これらはすべてテストの実行時にしか判断できない条件になります。 今まではXCTestではテストの結果は「成功」または「失敗」しかありませんでした。

そのため、実行環境によって動かないテストは求めている実行環境以外で動作したときに「成功」または「失敗」のどちらかになるようにするか、またはそのテスト自体を無効化するかといった選択肢しかありませんでした。

成功とすると、テストを実行していないのに問題ないという判定をすることになります。 失敗とすると、何も問題が発生していないのに落ちた原因をチェックするコストを使うことになります。

Xcode11.4からテストをスキップできるXCTSkipというAPIが追加されました。 これにより、特定の条件の際に自動テストをスキップできます。

XCTSkipを利用してテストのスキップ

このXCTSkipを利用することで、明示的にテストを特定条件の際にスキップできます。

例えば次のコード例のようにXCTSkipUnlessを利用することで、実行デバイスがiPadでなければテストをスキップできます。

func testSkipExample() throws {
    try XCTSkipUnless(UIDevice.current.userInterfaceIdiom == .pad, "iPad only")
    
    //なにかしらの処理
}

このようにテストをスキップさせた場合、Xcode上では次のように表示されます。 今までは「成功の緑」と「失敗の赤」だけでしたが、この「スキップのグレイ」が増えました。

f:id:swet-blog:20200813095549p:plain

また自動テストはCI/CDサービスで動かすと思います。 そのため、CLI上で動かしたときに生成されるテスト結果にもテストが成功・失敗の情報だけでなくスキップされた情報も必要になります。

利用できるテスト成果物として、Xcode11から追加されたResult Bundleがあります。 このResult Bundleは次のコマンドを用いることでJSONフォーマットとして情報を生成できます。

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

生成された情報の一部は次のようになります。 これを見るとわかりますが、testSkippedCount としてスキップの情報もあることがわかります。

  "metrics" : {
    "_type" : {
      "_name" : "ResultMetrics"
    },
    "errorCount" : {
      "_type" : {
        "_name" : "Int"
      },
      "_value" : "2"
    },
    "testsCount" : {
      "_type" : {
        "_name" : "Int"
      },
      "_value" : "3"
    },
    "testsFailedCount" : {
      "_type" : {
        "_name" : "Int"
      },
      "_value" : "1"
    },
    "testsSkippedCount" : {
      "_type" : {
        "_name" : "Int"
      },
      "_value" : "1"
    }
  }

このResult Bundleを活用することにより、CI/CDサービスで自動テストを実行した場合でも、テストの「成功」「失敗」「スキップ」といった情報を取得できます。

Result Bundleについては、以前本ブログで書いた記事を参考にしてもらえればと思います。

並列実行

必要な理由

並列実行は自動テストにおいてよくある実行時間の短縮の方法です。 テストを分割しておこなえるために、トータルとしての実行時間が短くできます。

XCTestにおいてもいろいろな方法で並列実行をおこなうことができますが、Xcode10からiOSシミュレーターの並列実行が容易にできるようになっています。

ただし、並列実行をすれば必ずしも実行時間が確実に短縮されるわけではありません。 ある特定のテストケースだけ実行時間が長いと、トータルの実行時間はそのテストケースに引っ張られてしまいます。

このXcode10から追加された並列実行の方法や注意点については、以前書いたSWETブログの記事を参考にしてください。

Xcode12における並列実行

Xcode11.4からxcodebuildコマンドを利用すると、iOS/tvOSの実機に対して並列実行を行うことができるようになりました。

これにより、iOSシミュレーター / iOS実機と並列実行をできるようになり、いろいろなパターンで自動テストを実行できるようになりました。

本機能による並列実行の注意点として、自動テストがどの端末に割り当てられるかは指定できません。 (XCTestでは実行するテスト/実行しないテストを指定できるので、どの端末にどのテストを指定するかを決めることによる並列実行もできますが、手動管理になるため大変です)

そのため、基本的には同一のデバイス・同一のOSバージョンで端末を用意しておくのが理想的です。

同じ端末を用意できずに、異なるデバイスやOSを使って自動テストの並列実行する場合は、デバイスやOSに依存するようなテストは実行しないのが望ましいです。

フィードバックからの情報取得

テストが常に成功するのであれば、テストからのフィードバックに付加的な情報はそこまで必要ないかもしれません。

しかし、テストは失敗するものです。 そのため、テストからのフィードバックで得られる情報は重要です。

フィードバックからの情報としては、次のような情報を得られることが重要になってきます。

  • どのように失敗したのか
  • なぜ失敗したのか
  • ソースコードのどこで失敗したのか

Xcode12ではテストの失敗時に上記のような情報をより分かりやすく提供してくれるようになっています。

ここでは次の3点について説明をしていきます。

  • テストの実行時間の制御(spindump)
  • アサーション
  • テスト失敗時の情報(XCTIssue)

テストの実行時間の制御(spindump)

テストの実行時間の制御によりテストが失敗した場合、どうして想定時間より長くかかったかを知る必要があります。

ハングした原因を推測することは難しいことが多いです。 Xcode12からテスト実行時間の制御によりテストが失敗した場合、テスト結果にspindumpが添付されるようになりました。

テスト結果のspindumpを開くことで、失敗したテストケース名があるはずです。 この情報をおうことで、最も多くの時間を費やしているメソッドを教えてくれます。

これにより、ハングした原因の調査の助けになります。

アサーション

テストが失敗した場合に、指定した期待する値と実際の値が異なっていたとして、それぞれの値がどのような意味を持つのかがわからないことがあります。

これがわかるようにアサーションメッセージを適切に使うことは重要です。 次のようにXCTAssertsではメッセージを設定できます。

XCTAssertEqual(expected, actual, "アサーションメッセージ")

このメッセージに適切な情報を含めておくことが重要です。 しかし、このメッセージに失敗したファイルパスを含める必要はありません。

Xcode12では失敗時の情報がより適切に表示されるようになりました。

例えば独自のアサーションメソッドを用意することがあると思います。 その場合、今までは次のようにテストケース側にアノテーションが付与されずに、実際に呼び出しているアサーションメソッド側にアノテーションが付与されます。

f:id:swet-blog:20200813095311p:plain

そのため、実際にどこでテストが失敗したのかがわかりづらいところがありました。 今までは、次のようにfileとlineを利用することで上記の画像の場所ではなく、テストケース側でアノテーションを表示できました。

func assertSample(file: StaticString = #file, line: UInt = #line) {
    XCTAssertTrue(false, file: file, line: line)
}

しかし、この対応でも問題は残ります。 この対応ではテストケース側にアノテーションが表示されます。 そのため、アサーションメソッド内のどのアサーションで失敗したかまではわかりません。

Xcode12ではこれらの問題が解決しています。

今回のような場合では、テストケース側にはグレイのアノテーションが表示され、アサーションメソッド側には赤いアノテーションが表示されるようになりました。

テスト失敗時の情報(XCTIssue)

Xcode12からテスト失敗時の情報が今までよりまとまっています。

今まではXCTestではテスト失敗時にrecordFailure APIを呼び出しています。 これは次にあるように4つの情報を保存しています。

func recordFailure(withDescription description: String, 
            inFile filePath: String, 
            atLine lineNumber: Int, 
            expected: Bool)

expectedはアサーションの失敗によってテストが失敗した場合はtrueで、例外の場合だとfalseになります。

Xcode12からはrecordFailureはdeprecatedになり、新しくrecord APIが追加され、このAPIが呼び出せるようになりました。

このAPIを見るとわかるように、今までのような値の渡し方ではなくXCTIssueにカプセル化されています。

そして、今までよりも複数の情報が追加されています。

これにより、自動テストを実行したXcode上や自動テストの成果物であるResult Bundleでテスト失敗時の情報がより細かく分かるようになりました。

このrecord APIはテストの失敗時に呼ばれるので、overrideしてカスタムなものを作ることもできます。

overrideできることで、情報を増やしたり絞ることもできます。 たとえば、Attachmentとしてログを別途追加するといったこともできます。

    override func record(_ issue: XCTIssue) {
        var issue = issue
        issue.add(XCTAttachment(string:  log))
        super.record(issue)
    }

Instruments

ここからは、片山がXcode12からのInstrumentsについて、業務でInstrumentsを利用している中で特に嬉しい変更について2つご紹介します。

xctraceの登場

今回Instrumentsにおいても大きな変更が入りました。 今までCLIから実行する際にはinstrumentsコマンドを用いていましたが、Xcode12でdeprecatedとなりました。 その代わりとしてxctraceコマンドが新たに追加されました。xctraceに新生したと同時に新たにできることが増えました。 そのxctraceで新たにできるようになったことで最も嬉しいと感じたのが、「Analysis Core Tables」のexportです。

Xcode11までのInstrumentsでは、計測した結果のtraceファイルを基本的にはInstruments App以外で見ることができませんでした。 そのため、計測した結果をGUIでチェックする必要がありました。 例えば端末温度を計測をした際はtraceファイルをInstrumentsで開いて、GUIを操作して問題ないかをチェックする必要があります。 (我々は、InstrumentsアプリをXCTestで動作させ、スクリーンショットやUI要素の情報を得ることで計測結果を簡易的に取得することをおこなっていました)

これらの計測結果のtraceファイルの内容をxctraceからXML形式でexportできるようになったため、exportしたXMLをパースして計測結果を機械的にチェックすることも可能になります。 毎日計測して結果を得たい場合にはとてもありがたい機能追加です。 細かい使い方についてはmanコマンドにしか詳細が無いのでぜひそちらを参照してみてください。

また、xctraceではかゆいところに手が届く変更も入りました。 instrumentsコマンドは以下のような少しだけ不便な部分がありました。

  • 選択できるtemplateの一覧をinstrumentsコマンドから見ることができない
  • 選択できる端末の一覧をinstrumentsコマンドから見ることができない

些細な問題かもしれませんが、ちょっとだけ不便なところがxctraceでは改善されました。 templateと端末の一覧をxctraceのコマンドから簡単に調べることができます。

xctraceについては当然まだベータ版であり、不具合と思われる報告もされています。 気になる方はApple Developer Forumで探してみてください。

保存 / 読み込み速度の改善

Instruments App本体でも少しうれしい改善がありました。 traceファイルは長時間計測するとサイズも大きくなっていき、保存/読み込みにかなりの時間が掛かっていました。 その問題となっていた保存/読み込みのパフォーマンスが次のように改善されました。

  • 保存時:最大40%高速化
  • 読み込み時:最大80%高速化

あくまで最大と書いてあり状況によって変化するため、私個人として試したところでは劇的に早くなったとは感じないが、少しは早くなったかなという感想です。 Resolvedの項目に記載されてるため、Appleとしても気にしていたと思われます。 今後のアップデートでも保存/読み込み速度の改善が継続的にされると嬉しいです。

おわりに

今回、WWDC2020の動画からSWET視点で選んで上述した2点について紹介しました。

WWDC2020の動画を聞いた感想として、すでにある機能が今まで以上により使いやすい形でまとめられ提供されるようになったと感じています。

また、まだXcode12がリリース前の関係で変わることもあるかと思いますし、伝えきれなかった情報もあります。 それらについては正式にリリースした時点で追加記事を書ければと思います。

参考資料

おまけ

SWETではテストに特化した勉強会としてTest Nightを定期的に開催しています。

昨今の状況もあり開催できていませんでしたが、オンラインでの開催をおこなう予定です。 それにともない、Test Nightとは別の「Test Online」というグループをconnpassで作成しています。

このグループでの勉強会を近日中にconnpassを公開するので、ぜひとも参加していただければと思います。

おまけ2

今回、Instruments周りについて紹介した理由としてパフォーマンス計測に力を入れて取り組んでいるというのがあります。

今までの取り組みについて、iOSDC Japan 2020で登壇予定ですのでぜひ視聴していただければと思います。

継続的にアプリのパフォーマンスを計測する by kariad | トーク | iOSDC Japan 2020 - fortee.jp