The Xcode UI testing framework has been with us since Xcode 7, but I still find it a struggle to use. Apple has the usual API documentation but practical examples are hard to find so I often stumble around when using it on a real project.
Here are my notes on dealing with one common roadblock - the system interrupting to ask for permission for something.
Last updated: Aug 31, 2020
UI Testing
I want to test this initial home screen when my App launches:
The view has a label, configured in a storyboard with an accessibility identifier:
My UI test might look like this:
// UITests.swift
import XCTest
class UITests: XCTestCase {
let app = XCUIApplication()
override func setUp() {
continueAfterFailure = false
app.launch()
}
func testHomeScreen() {
let textQuery = app.staticTexts["WelcomeText"]
XCTAssertTrue(textQuery.exists, "Missing welcome text")
}
}
This is fine until I decide my App needs to support local notifications. So I request authorization after my App has launched:
let center = UNUserNotificationCenter.current()
center.requestAuthorization(options: [.alert]) {
(granted, error) in
// ...
}
Now my UI test leaves a system dialog open on the Simulator the first time it runs:
This is annoying, especially if I am using a tool like fastlane to capture screenshots. How do you dismiss these system alerts?
Handling UI Interruptions
Assuming you don’t give up and reach for stack overflow you might stumble on the documentation for Monitoring UI Interruptions in XCTestCase
. Adding this to our setup
method is a good start:
override func setUp() {
continueAfterFailure = false
app.launch()
addUIInterruptionMonitor(withDescription: "System Dialog") {
(alert) -> Bool in
alert.buttons["Allow"].tap()
return true
}
app.tap()
}
The XCTestCase
instance method addUIInterruptionMonitor
registers a handler to call when the UI is interrupted by an alert. The handler is passed an XCUIElement
for the alert which we can use to find and tap the “Allow” button. Return true
to indicate you handled the alert.
The one caveat is that you need to interact with the App to trigger the handler. Tapping the app works fine in this case:
app.tap()
Sometimes a tap might trigger an unwanted action. Depending on your view hierarchy you might need to tap on a specific view or bar you know is safe:
app.navigationBars.firstMatch.tap()
What if we have multiple system dialogs? For example, if my App also requests access to the microphone:
Note that the system dialog has an “OK” button this time where the notification dialog had an “Allow” button. Our handler is called for both alerts so one approach can be to check for either button:
addUIInterruptionMonitor(withDescription: "System Dialog") {
(alert) -> Bool in
let okButton = alert.buttons["OK"]
if okButton.exists {
okButton.tap()
}
let allowButton = alert.buttons["Allow"]
if allowButton.exists {
allowButton.tap()
}
return true
}
If we don’t care what the dialog is we could also click the second button to dismiss it without checking the title (which helps avoid localization issues):
let button = alert.buttons.element(boundBy: 1)
if button.exists {
button.tap()
}
If we want to be precise about which permissions we are granting the test target we can look for the permission text in the alert:
let notifPermission = "Would Like to Send You Notifications"
if alert.labelContains(text: notifPermission) {
alert.buttons["Allow"].tap()
}
I have a small helper function defined in an extension of XCUIElement
to test for a label containing text with a predicate:
extension XCUIElement {
func labelContains(text: String) -> Bool {
let predicate = NSPredicate(format: "label CONTAINS %@", text)
return staticTexts.matching(predicate).firstMatch.exists
}
}
Note: You’ll need to localize the expected permission strings if you run in non-English locales.
You can also split the different alerts into their own handlers:
addUIInterruptionMonitor(withDescription: "Local Notifications") {
(alert) -> Bool in
let notifPermission = "Would Like to Send You Notifications"
if alert.labelContains(text: notifPermission) {
alert.buttons["Allow"].tap()
return true
}
return false
}
addUIInterruptionMonitor(withDescription: "Microphone Access") {
(alert) -> Bool in
let micPermission = "Would Like to Access the Microphone"
if alert.labelContains(text: micPermission) {
alert.buttons["OK"].tap()
return true
}
return false
}
The handlers are called in the reverse order they were added until one of them returns true
. To remove a handler you need to store the token returned when you add the handler:
let handler = addUIInterruptionMonitor(...)
// ...
removeUIInterruptionMonitor(handler)
I Can’t See Why It’s Not Working?
One final tip to save some frustration. What if you want to test the case where the user does not allow the permission? Easy enough you say, change the handler to click on the “Don’t Allow” button:
So something like this:
if alert.labelContains(text: notifPermission) {
alert.buttons["Don't Allow"].tap()
}
Except it doesn’t work. The Xcode debug console shows the problem, but you have to look carefully:
Find: Descendants matching type Button
Find: Elements matching predicate '"Don't Allow" IN identifiers'
Assertion Failure: UITests.swift:63: No matches found for Find: Elements matching predicate '"Don't Allow" IN identifiers' from input {(
Button, label: 'Don’t Allow',
Button, label: 'Allow'
Can you see it? It may depend on how you are reading this post, but the apostrophe in the alert is a right single quotation mark ’
(U+2019) not the usual straight apostrophe '
(U+0027). The corrected query code (assuming you didn’t give up and query for the first button):
if alert.labelContains(text: notifPermission) {
alert.buttons["Don’t Allow"].tap()
}
Further Reading
This session from WWDC 2020 is a great summary:
A recap of Xcode UI testing from the WWDC 2015 session: