概要
XcodeによるiOSアプリのUIテストにて、下記のようにアラートが出るケースでの制御方法を記載します。
対応version
- Xcode12以後とXcode12より前で対応方法が変わります
- 2021/07時点の挙動です
通常の対応方法(Xcode12より前)
通常であれば、addUIInterruptionMonitor
という関数を利用して制御します。
Xcode12以後の場合、おそらくバグで上記インターフェースが機能しません。
詳細はこちら
なので、Xcode12以後で開発している方は、後述の回避策をご利用ください。
とはいえ、addUIInterruptionMonitor
を利用するのが正規の方法のようなので、こちらの対応方法も記載しておきます。
addUIInterruptionMonitorの挙動
- addUIInterruptionMonitorを下記のように宣言しておくと、アラート(通知も)が表示された際にキャッチアップし、ブロック内の処理が走るようになります。
- この関数は重ねて宣言可能であり、アラートが発生した際には新しく宣言されたものから順にブロックが実行されます。
- ブロック内で
return true
された場合、処理は完了とみなされ、それ以前の宣言は無視されます。
以下の例では 宣言C
が最も新しく宣言されているので、アラート発生時にここのブロックから処理が走ります。
そして宣言Cのブロック内で return false
を返しているので「制御失敗」とみなされ、次に新しい 宣言B
のブロック処理に移ります。
宣言Bのブロック処理では return true
しているので、「制御成功」とみなされ、次に新しい 宣言A
のブロック処理は走りません。
// 宣言A
addUIInterruptionMonitor(withDescription: "宣言Bで制御完了なので走らない") { element -> Bool in
return true
}
// 宣言B
addUIInterruptionMonitor(withDescription: "アラート発生時に実行される") { element -> Bool in
// return true だと制御成功とみなされ、より前に宣言されたブロックの実行は走らない(ここでは宣言Aが無視される)
return true
}
// 宣言C
addUIInterruptionMonitor(withDescription: "アラート発生時に実行される") { element -> Bool in
// return false だと制御失敗とみなされ、より前に宣言されたブロックの実行に移る(ここでは宣言Bの実行に移る)
return false
}
また、addUIInterruptionMonitor
は一意のトークンを返却するので下記のようにテストケース内で利用した後に削除することも可能です。
let token = addUIInterruptionMonitor(withDescription: "ここでしか使わないアラート処理") { alert -> Bool in
// 想定されるアラートの押したいボタンテキストを記述
let allow = alert.buttons["許可する"]
if allow.exists {
allow.tap()
return true
}
return false
}
removeUIInterruptionMonitor(token)
使いどころ
- 主に全体共通で処理したいアラートについては、
setup()
もしくはsetUpWithError()
に記載しておくとよいかもしれません。
override func setUp() {
addUIInterruptionMonitor(withDescription: "全体で使う予期せぬアラート用") { element -> Bool in
// Cancel ボタンがあればタップして閉じる
let cancel = alert.buttons["Cancel"]
if cancel.exists {
cancel.tap()
return true
}
// Allow ボタンがあればタップして閉じる
let allow = alert.buttons["Allow"]
if allow.exists {
allow.tap()
return true
}
return false
}
}
override func setUpWithError() throws {
// もしくはここ
}
- 特定の場合でしか出ないアラートを処理する時は、テストケース内に書いてしまう
func testExample() throws {
XCTContext.runActivity(named: "正常系テスト") { _ in
let token = addUIInterruptionMonitor(withDescription: "ここでしか使わないアラート処理") { alert -> Bool in
// 想定されるアラートの押したいボタンテキストを記述
let allow = alert.buttons["許可する"]
if allow.exists {
allow.tap()
return true
}
return false
}
removeUIInterruptionMonitor(token)
XCTAssertTrue(true)
}
}
Xcode12以後の対応方法(addUIInterruptionMonitor反応しない時の回避策)
前述の通り、本来であれば addUIInterruptionMonitor
にてアラートを制御したいところだが、Xcode12だとこのインターフェースが反応しないので、下記のような回避策をとる。
- Springboardのアプリケーション(システムアラートが出た場合このアプリケーションに該当する)を呼び出し、そこから任意のボタンを検知する Springboardについて
- 上記で反応しない場合(システムアラートではない場合)は実行中のアプリのアラート画面を検索し、任意のボタンを検知する
var app: XCUIApplication!
override func setUp() {
app = XCUIApplication()
}
func testExample() throws {
app.launch()
// システムアラートを管理している springboard のアプリケーションをキャッチアップ
let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
// springboard のアプリケーションにアラートが表示されている想定なので、そこから任意のボタンを検知する(ここでは「許可する」ボタン)
let systemAllowBtn = springboard.buttons["許可する"]
// テスト実行中のアプリでもアラートをキャッチし、任意のボタンを検知する(ここでは「許可する」ボタン)
let allowBtn = app.alerts.firstMatch.buttons["許可する"]
// システムアラートの方のボタンが検知できるか2秒まで待つ
if systemAllowBtn.waitForExistence(timeout: 2) {
// あればボタンをタップして閉じる
systemAllowBtn.tap()
// アプリの方のボタンが検知できるか2秒まで待つ
} else if allowBtn.waitForExistence(timeout: 2) {
// あればボタンをタップして閉じる
allowBtn.tap()
}
XCTAssertTrue(true)
}
使いどころ
こちらの対応方法の場合、ランタイムに依存するのでアラートが出るであろう箇所に仕込む必要があります。
ある程度タイミングを想定できていないと無理なので、アプリ全般の予期せぬアラートに対応させるには不向きです。。。
無念。