UIテストにおける割り込みやアラート処理 – WWDC2020

Session概要

Appのインタフェースへの割り込みの可能性を予測する方法、それらを識別するスマートテストのビルド方法について。
UIへの割り込みのタイミングは定まっておらず、一般的にはオンボーディングや初回起動時に起こり、見つけ出すのが難しいことがあります。
割り込みを理解し、UI割り込みハンドラを使ってより強力なテストを書いたり、予想されるアラートの管理方法について。 https://developer.apple.com/videos/play/wwdc2020/10220/

UI割り込みとは何か?

UITestがインタラクションする別の要素に対して、アクセスを中断する動作

例えば、UITest実行中にpush通知が来たとしてもUITestの範囲外であるため反応出来ません。ですが、UITestにおいてNavigationBarのアクションを実行するテストコードがあった場合に失敗します。
そのためこのケースではpush通知でのバナーがUI割り込みとなります。
その他に、アラート, ダイアログ, ウィンドウなどがあります。
注意点はUI割り込みは予測不能で法則に従った挙動にならないということ。 このような問題に対処するためにUI割り込みにも反応出来るようにしておきます。


UI割り込みのハンドリング方法

  • 割り込みハンドラはXCTestを呼び出すクロージャで複数の割り込み登録が可能
  • 割り込みハンドラは登録した順番に呼ばれる
  • 失敗した場合(false)は次のハンドラを呼ぶ
  • UI割り込みハンドラはテスト終了時に自動 or 手動で閉じることが可能

iOSのビルドイン割り込みハンドラ

  • アラート
    • キャンセル、デフォルトボタン
  • バナー通知(Xcode12から)
    • スワイプ or 待機による自動非表示

MacOSのビルドイン割り込みハンドラ

  • ユーザ許可ダイアログ
    • 許可しない
  • Bluetooth設定アシスタント
    • ウィンドウを閉じる

Sample Code

XCTestでUI割り込み時に再試行のハンドリングを行う実装
このXCUITestを実行時に、例えば下記のようにアプリ起動時にアラートが表示された場合に失敗するため、アラートが表示された時に addUIInterruptionMonitor でハンドリング出来るようにしています
override func setUpWithError() throws {
  addUIInterruptionMonitor(withDescription: "Handle recipe update failures") { element -> Bool in
    let retryButton = element.buttons["Retry"].firstMatch
    
    // 割り込み要素がアラートで、「再試行」ボタンがあれば再試行を押下する
    if element.elementType == .alert && retryButton.exists {
        retryButton.tap()
        return true
    } else {
        return false
    }
  }
}

func testRecipeDetailsNavigation() throws {

    let app = XCUIApplication()
    app.launch()

    let pancakeRecipe = app.cells.staticTexts["Fluffy Pancakes"].firstMatch
    pancakeRecipe.tap()

    // In the detail view
    let detailTitle = app.navigationBars.staticTexts["Fluffy Pancakes"].firstMatch
    XCTAssert(detailTitle.waitForExistence(timeout: 30))

    let expectedImage = app.images["Fluffy Pancakes Image"].firstMatch
    XCTAssert(expectedImage.exists)

    let ingredientsTitle = app.staticTexts["Ingredients Title"].firstMatch
    XCTAssert(ingredientsTitle.exists)

    let ingredientsContent = app.textViews["Ingredients Content"].firstMatch
    XCTAssert(ingredientsContent.exists)
    XCTAssert((ingredientsContent.value as! String).count > 0)

    let instructionsTitle = app.staticTexts["Instructions Title"].firstMatch
    XCTAssert(instructionsTitle.exists)

    let instructionsTextView = app.textViews["Instructions Content"].firstMatch
    XCTAssert(instructionsTextView.exists)
    XCTAssert((instructionsTextView.value as! String).count > 0)

    // Make sure we received the latest instructions:
    let expectedInstructions = """
                               1. Mix the flour, sugar, baking powder and salt.

                               2. Pour the milk, melted butter and egg into a well in the center and mix.

                               3. Heat a frying pan (don't forget to add a little bit of oil, grandson!), and fry until they're golden.

                               4. Optionally add maple syrup and/or fruits (they're healthy!).
                               """
    XCTAssertEqual((instructionsTextView.value as! String), expectedInstructions)

    // Go back to the list of recipes
    let backButton = app.navigationBars.buttons["Grandma's Recipes"].firstMatch
    backButton.tap()

    XCTAssert(pancakeRecipe.waitForExistence(timeout: 30))
}

予測可能なアラートの最適なハンドリング方法

アラートのほどんどのケースは予測可能であるため、割り込みとして処理しないようにしたい場合があります。

例えば、下記のようなTableViewからcellを一行削除しようとした際に、「本当に削除しますか?」などのようなアラートが表示されるケースです。

予測不能なUI割り込みのハンドリング

Bluetoothや位置情報、マイクなどはユーザのプライバシーに注意が必要でありこれらにアクセスする前に許可を求め、明示的に許可をしたか判断する必要があります。
そのため、テストケースは複数用意する必要があり、ユーザが許可 or 拒否した場合のアプリの挙動はどうなるのが適切か。
しかし、ユーザが許可したかどうかはシステム設定側で保存されるため細分化されたデータ試験が不可能となります。 このようなケースを回避するために、保護されたリソースの権限をリセットするAPIが用意されている。
resetAuthorizationStatus APIを利用することでXCTest時のアプリの状態を初期にした状態でテストが可能であり、副作用を起こさないように出来る。 ただし、resetAuthorizationStatus の実行後はアプリが終了するため再起動が必要。
func testAddingPhotosFirstTime() throws {
    let app = XCUIApplication()
   
    // ここで写真へのアクセスをリセットしている
    app.resetAuthorizationStatus(for: .photos)

    app.launch() // 再起動する

    // Test code…
}

リセット可能なリソース


iOSの場合はキーボードのネットワーク接続やBluetoothもサポートしており、iOS14からはヘルスケアも追加。
MacOSでは様々なディレクトリへのアクセス権限など。
最新情報をチェックしよう!