WWDC
Swift

【まとめ】What's New in Testing【WWDC 2017】

More than 1 year has passed since last update.


はじめに

この記事では、WWDC 2017でのセッション What's New in Testing の内容をまとめます。

このセッションでは、Xcode9から登場する多種多様な新しいAPIが発表され、UI Testのパフォーマンス改善などの技術的な進化も紹介されました。

macOSやtvOSなどに関するトピックもありましたが、僕自身がそれらに詳しくないのと、発表のほとんどがiOS関連だったので、iOS関連のトピックに関してのまとめになります。

Xcode9にアップデートして強化されたUI Testを利用したい方や、これからUI Testを導入しようと思っている方の参考になれば嬉しいです。

それでは、早速まとめに入りましょう。


セッションのまとめ


Xcode9でのEnhancements


並列テスト


  • 複数のシナリオを1回のビルドで並行してテストできるようになる。

  • テスト実行スピードが大幅に上がる。


言語と地域の指定(Localization)


  • テストを走らせるときに言語と地域を指定できるようになる。


Async testing(非同期処理のテスト)


expectationwaitForExpectationなどについて


  • Xcode6から提供されているAPI

  • 非同期処理の完了を待機するために用いられる。

  • 問題点


    • タイムアウトがすべてテスト失敗と扱われる

    • テストオブジェクト自体を待つからテスト失敗の原因特定が難しい

    • 待機をネストできない



今まではこんな感じでテストしていました。

let document = UIDocument(fileURL: documentURL)

let documentExpectation = expectation(description: "Document opened")
document.open() { success in
XCTAssert(success, "Failed to open file")
documentExpectation.fulfill()
}
waitForExpectations(timeout: 10)


XCTWaiterがXcode9から登場。

次のようなことが可能になった。

- 待機リストの管理

- XCTWaiterDelegateへのコールバック

今まではすべてのexpectationに対してぼんやりと待機していた。

// Test case waits implicitly

waitForExpectations(timeout: 10)

Xcode9では次のようにフレキシブルにテストを処理できる。

// expectationを指定してテストケースをwaitさせられる

wait(for: [documentExpectation], timeout: 10)

// コールバック処理が書ける
XCTWaiter(delegate: self).wait(for: [documentExpectation], timeout: 10)

// waitの返り値によって処理を分けられる
let result = XCTWaiter.wait(for: [documentExpectation], timeout: 10)
if result == .timedOut {
// handling the timeout…
}


XCTestExpectationも進化

次のように変更された。


  • initializerがXCTestから分離され、publicに。

  • 複数回fulfillされるexpectationにその回数を保持するプロパティが登場。

  • タイムアウトまでにfulfillされてしまうと逆にテストが失敗するInverted behaviorが登場。

  • オプショナルなフラグを利用してfulfillされる順番も指定できる。


Multi-app testing


XCUIApplicationについて


  • このクラスのインスタンスを利用すると、アプリの起動・終了やUI部品などへのクエリ作成、タップなどの同期イベントなどをシミュレーションできて便利。


    • しかし、Test Targetを指定しているので他のアプリケーションをテストすることができなかった。




Xcode9でのXCUIApplication


  • 複数のアプリを起動し、アプリ間でデータの受け渡しが可能に。

  • 複数の設定で複数のシナリオをテストできるように。

上記を実現するために次のような変更が行われた。



  • init(bundlerIdentifier: String)


    • bundle IDを指定してアプリを起動するInitializerが登場。




  • func activate()


    • バックグラウンドからフォアグラウンドにアプリを持ってくる、もしくは起動されていなかったらそのアプリのXCUIApplicationインスタンスを作成するメソッドが登場。




  • var state: XCUIApplication.State { get }


    • アプリの今の状態を保持するプロパティが登場。



このように複数アプリを起動でき、アプリ間の連携も可能に。

let readerApp = XCUIApplication(bundleIdentifier: "com.mycompany.Reader")

let writerApp = XCUIApplication(bundleIdentifier: "com.mycompany.Writer")

readerApp.launch()
// interact with first app
writerApp.launch()
// interact with second app
readerApp.activate()
// return to first app without relaunching


デモ

デモでは、新登場のAPIを使用してメッセージをサーバへ送信するアプリとサーバからメッセージを受け取るアプリの2つのアプリを同時にテストしていました。

少しのAPI追加でここまでテストの幅が広がるのはすごいですね。


UI testing performance(UI Testのパフォーマンス改善)


クエリの仕組みとその問題点

UI部品を検索する際の手順は次のようになっている。


  1. test processがアプリケーションへ現在のデータのスナップショットをリクエスト

  2. アプリケーションはデータのスナップショットをキャプチャし、シリアライズしてtest processに返す

  3. test processが受け取ったスナップショットを開封し、クエリを用いてUI部品を検索する

この問題点には以下のようなものがある。


  • スナップショットをキャプチャするのに時間がかかる


    • tableに数千のrowがあったり、巨大なcollection viewがあったりするとテスト失敗を招くタイムアウトを引き起こす。



  • スナップショットの容量がメモリを圧迫する


    • アプリが落ちてしまう。




どう解決したか


  • 第一の案:Remote Query


    • test processが容量の小さいクエリ自身をアプリケーションに送信してクエリ評価をアプリケーションが行い、結果(これも容量が小さい)のみをtest processに返すことで送受信のオーバーヘッドを無くした。

    • 速度が20%上昇し、メモリ使用率を30%減少させた。

    • でも満足できなかった。



  • 第二の案:Query Analysis


    • シンプルに集めるUI部品のデータを少なくして、スナップショットの容量を小さくする。

    • 速度が50%上昇し、メモリ使用率を35%減少させた。

    • でももっといいアイデアがないかなと思った。



  • 第三の案:First Match



    • var firstMatch: XCUIElement { get }の登場。

    • 従来はスナップショット全域を検索していたが、最初にマッチしたときにクエリを終了するようにした。

    • 速度は数十分の一に、メモリ使用率はほぼ0になった。

    • しかし、これを利用すると曖昧なクエリを検出できない。

    • 次のように、できるだけ曖昧さをなくすクエリの書き方が重要になってくる。



let button = app.buttons.firstMatch    // not a good idea

let button = app.buttons["Done"].firstMatch // better
let button = app.navigationBars.buttons["Done"].firstMatch // best


NSPredicateの使用について



  • NSPredicateを使用すると、Remote QueryもFirst MatchもQuery Analysisも使えない。



    • Format StringNSExpressionに置き換える。

    • 置き換えられない状況があればAppleに連絡してくれ、とのこと。笑




Activities, attachments, and screenshots


Activities

XCTContext.runActivity(named: "Compose coffee message") { _ in

// Compose and send a new message
let composeView = writerApp.textViews["Compose message"]
composeView.tap()
composeView.typeText("Any good coffee places around McEnery? ☕ /cc @jane")
writerApp.buttons["return"].tap()
writerApp.buttons["send"].tap()
}

上記のようにXCTContext.runActivity(named name: String, block: (XCTActivity))のブロックに一連の処理を渡すと、テストレポートの中でブロック内の処理をグルーピングして表示してくれる。


Attachments

テストの内容を考察しやすいように、テスト結果を表す様々な種類のリッチなデータをテストレポートに添付できるようになった。

その代表例が次に紹介するScreenshotsである。


Screenshots


XCUIScreenshotsProvidingプロトコルの登場



  • XCUIElementXCUIScreenscreenshot()メソッドが使用可能に。

  • ボタンならボタンのサイズ、ウインドウならウインドウのサイズにクリップされたスクリーンショットが取得可能に。

  • テストを通過するとそれらのスクリーンショットは自動的に削除される。テストに失敗すると残る。


    • Schemeでこの設定は変更可能。




デモ


  • Test DiamondをCtrl-ClickしてJump to Reportを選択するとテストレポートを見れる。

  • 実際にスクリーンショットをキャプチャするには次のようにする。


    • このようにしてテストを実行したあとにテストレポートの「Gather Screenshots」の欄を見ると「目」のマークがついたスクリーンショットがある。

    • この「目」のマークをクリックしてもスクリーンショットは確認できるが、Assistant Editorを開いても確認することができる(拍手)。



XCTContext.runActivity(named: "Gather Screenshots") { activity in

// Capture the full screen
let mainScreen = XCUIScreen.main
let fullScreenshot = mainScreen.screenshot()
let fullScreenshotAttachment = XCTAttachment(screenshot: fullScreenshot)
fullScreenshotAttachment.lifetime = .keepAlways // テストが成功してもスクリーンショットが消えない
activity.add(fullScreenshotAttachment)

// Capture just the first cell
let cell = readerApp.cells.element(boundBy: 0)
let firstCellScreenshot = cell.screenshot()
let firstCellScreenshotAttachment = XCTAttachment(screenshot: firstCellScreenshot)
firstCellScreenshotAttachment.lifetime = .keepAlways
activity.add(firstCellAttachment)
}


おわりに

いかがでしたでしょうか。

XCTestを用いたテストは不完全な部分が多くて手を出している人も少ないような気がしますが、Xcode9からは大幅に強化されるみたいなので、これをきっかけにUI Testを導入してみてはいかがでしょうか。

最後に、記事の内容に誤りなどがございましたら、気軽に、優しく、コメントしていただけると嬉しいです。

それでは、最後までお読みいただき、ありがとうございました!