Handling UI Interruptions in iOS UI Tests with XCTest

When you are writing UI tests for an iOS it is likely that you will encounter cases where your test needs to interact with an alert presented by the system.

XCTest provides API to handle these cases with addUIInterruptionMonitor(withDescription:handler:) and the corresponding method to remove this monitor on XCTestCase.

If your app does some setup to explain why a permission is required your UI flow may be as follows:

  1. Present a view controller explaining the need for the permission being requested.
  2. The user taps a button to proceed with requesting the permission.
  3. You request authorization from the relevant API, causing the permission prompt alert to be shown by the system.

To test this with a UI test you might do the following:

  1. Navigate to the relevant screen.
  2. Add the UI interruption monitor to handle the expected alert.
  3. Tap the button to grant permission.
  4. Expect the next screen depending on the outcome of the permission grant.

You will likely not reach step 4 with your test initially.

To illustrate why this happens it may be helpful to study the transcript of a passing test case test case:

Test Case '-[SimpleUITestUITests.SimpleUITestUITests testGrantingPermissions]' started.
    t =     0.00s Start Test at 2019-04-25 07:33:34.747
    t =     0.06s Set Up
    t =     0.07s     Open sean.systems.SimpleUITest
    t =     0.10s         Launch sean.systems.SimpleUITest
    t =     1.53s             Wait for accessibility to load
    t =     2.65s             Wait for sean.systems.SimpleUITest to idle
    t =     3.98s Tap "Grant Permission" Button
    t =     3.98s     Wait for sean.systems.SimpleUITest to idle
    t =     4.01s     Find the "Grant Permission" Button
    t =     4.05s         Check for interrupting elements affecting "Grant Permission" Button
    t =     4.05s     Synthesize event
    t =     4.15s     Wait for sean.systems.SimpleUITest to idle
    t =     4.62s Tap Target Application 'sean.systems.SimpleUITest'
    t =     4.62s     Wait for sean.systems.SimpleUITest to idle
    t =     4.65s     Find the Target Application 'sean.systems.SimpleUITest'
    t =     4.67s         Check for interrupting elements affecting "SimpleUITest" Application
    t =     4.67s             Wait for com.apple.springboard to idle
    t =     4.70s             Snapshot accessibility hierarchy for app with pid 3409
    t =     4.75s             Find: Descendants matching type Alert
    t =     4.77s         Handle interrupting element
    t =     4.77s             Find the "Always Allow" Button
    t =     4.77s                 Snapshot accessibility hierarchy for app with pid 3409
    t =     4.82s                 Find: Descendants matching type Alert
    t =     4.82s                 Find: Identity Binding
    t =     4.82s                 Find: Descendants matching type Button
    t =     4.83s                 Find: Elements matching predicate '"Always Allow" IN identifiers'
    t =     4.84s             Tap "Always Allow" Button
    t =     4.84s                 Wait for com.apple.springboard to idle
    t =     4.87s                 Find the "Always Allow" Button
    t =     4.87s                     Snapshot accessibility hierarchy for app with pid 3409
    t =     4.91s                     Find: Descendants matching type Alert
    t =     4.91s                     Find: Identity Binding
    t =     4.91s                     Find: Descendants matching type Button
    t =     4.91s                     Find: Elements matching predicate '"Always Allow" IN identifiers'
    t =     4.91s                     Check for interrupting elements affecting "Always Allow" Button
    t =     4.91s                         Snapshot accessibility hierarchy for app with pid 3409
    t =     4.94s                         Find: Descendants matching type Alert
    t =     4.95s                 Synthesize event
    t =     5.04s                 Wait for com.apple.springboard to idle
    t =     5.10s             Waiting 10.0s for "Allow “SimpleUITest” to access your location?" Alert to not exist
    t =     6.11s                 Checking `Expect predicate `exists == 0` for object "Allow “SimpleUITest” to access your location?" Alert`
    t =     6.11s                     Checking existence of `"Allow “SimpleUITest” to access your location?" Alert`
    t =     6.11s                         Snapshot accessibility hierarchy for app with pid 3409
    t =     6.14s                         Find: Descendants matching type Alert
    t =     6.66s         Check for interrupting elements affecting "SimpleUITest" Application
    t =     6.67s     Synthesize event
    t =     6.75s     Wait for sean.systems.SimpleUITest to idle
    t =     6.78s Waiting 10.0s for "Permission Granted" StaticText to exist
    t =     7.79s     Checking `Expect predicate `exists == 1` for object "Permission Granted" StaticText`
    t =     7.79s         Checking existence of `"Permission Granted" StaticText`
    t =     7.81s Tap "Permission Granted" StaticText
    t =     7.81s     Wait for sean.systems.SimpleUITest to idle
    t =     7.84s     Find the "Permission Granted" StaticText
    t =     7.85s         Check for interrupting elements affecting "Permission Granted" StaticText
    t =     7.86s     Synthesize event
    t =     7.94s     Wait for sean.systems.SimpleUITest to idle
    t =     7.96s Tear Down
Test Case '-[SimpleUITestUITests.SimpleUITestUITests testGrantingPermissions]' passed (8.166 seconds).

First the Grant Permission button is tapped, causing the iOS permission request alert to be presented.

Next, the app is tapped. This causes the interruption monitor to inspect the alert and perform the actions defined in the handler — because the alert is blocking interaction with the app.

Notice also that the interruption monitor is only consulted after the UI test runner matches an element to interact with. If you attempt to match an element which is only expected to be present after the permission is granted or denied your test will not succeed.

Thus this presents two options for ensuring the interruption handler is executed during a UI test case:

  • Tap on an existing element (such as the button causing the alert's presentation)
  • Tap anywhere on the screen (this should succeeded in any case but may be less desirable)

Configuration: Xcode 10.2 (10E125)