NEW: Check out my podcast with Sean Allen, Swift over Coffee! >>

Xcode UI Testing Cheat Sheet

Paul Hudson       @twostraws

User interface testing is pretty much the ultimate integration test, because you’re seeing the app exactly how users do – there’s no explicit knowledge of how code is structured as with unit tests, and you can’t add mocks or stubs to isolate functionality.

Instead, UI tests access your app using iOS’s accessibility system: they scan the interface for controls you request, manipulate them to follow instructions you’ve provided, then make assertions about the end state. This makes them more fragile than unit tests because small UI changes can cause tests to fail, but on the other hand do provide excellent proof that your app workflows function as intended.

Previously I’ve written about how to test your user interface using Xcode, so in this article we’re going to cut to the chase: here are some quick code snippets that help you solve a variety of problems using Xcode’s UI testing system.

Finding elements

If you only have one a specific element in your interface, you can use these accessors to find them:

app.alerts.element
app.buttons.element
app.collectionViews.element
app.images.element
app.maps.element
app.navigationBars.element
app.pickers.element
app.progressIndicators.element
app.scrollViews.element
app.segmentedControls.element
app.staticTexts.element
app.switches.element
app.tabBars.element
app.tables.element
app.textFields.element
app.textViews.element
app.webViews.element

Note: staticTexts refers to labels on iOS.

For more specific elements, set an accessibility identifier like this:

helpLabel.accessibilityIdentifier = "Help"

You can then find it like this:

app.staticTexts["Help"]

The accessibility identifier is designed for internal use only, unlike the other two accessibility text fields that are read to the user when Voiceover is activated.

Advanced queries

XCTest runs queries on the app’s user interface, attempting to find some piece of UI. While the queries above are fine when you’re just starting, there are lots of more advanced queries we can run to find specific elements:

// the only button
app.buttons.element

// the button titled "Help"
app.buttons["Help"]

// all buttons inside a specific scroll view (direct subviews only)
app.scrollViews["Main"].children(matching: .button)

// all buttons anywhere inside a specific scroll view (subviews, sub-subviews, sub-sub-subviews, etc)
app.scrollViews["Main"].descendants(matching: .button)    

// find the first and fourth buttons
app.buttons.element(boundBy: 0)
app.buttons.element(boundBy: 3)

// find the first button
app.buttons.firstMatch

Some of those seem similar, so let me clarify a few things:

  • If you use element to read precisely one element, and there is more than one matching element (e.g. several buttons) your test will fail.
  • firstMatch will return the first item that matches your query, even if you have more than one matching element.
  • On the plus side, this makes firstMatch much faster than element because it won’t check for duplicates. On the down side, using firstMatch won’t warn you of accidental duplicates.
  • children(matching:) only reads immediate subviews, whereas descdendant(matching:) reads all subviews and subviews of subviews.
  • For that reason, children(matching:) is significantly more efficient than descdendant(matching:).

Remember, Xcode literally scans your user interface to discover what’s available. UI test code is a series of instructions telling Xcode what to look for, so the more precise we can be the better.

Interacting with elements

XCTest gives us five different methods that trigger taps on elements:

  1. tap() triggers a standard tap, which will trigger buttons or active text fields for editing.
  2. doubleTap() taps twice in quick succession.
  3. twoFingerTap() uses two fingers to tap once on an element.
  4. tap(withNumberOfTaps:numberOfTouches:) lets you control tap and touch count at the same time.
  5. press(forDuration:) triggers long presses over a set number of seconds.

There are also specific methods for gesture control:

  • swipeLeft(), swipeRight(), swipeUp(), and swipeDown() execute single swipes in a specific direction, although there is no control over speed or distance.
  • pinch(withScale:velocity:) pinches and zooms on something like a map. Specify scales between 0 and 1 to zoom out, or scales greater than 1 to zoom in. Velocity is specified as “scale factor per second” and causes a little overscroll after you zoom – use 0 to make the zoom precise.
  • rotate(_: withVelocity:) rotates around an element. The first parameter is a CGFloat specifying an angle in radians, and the second is radians per second.

So, you can tap on the only text field like this:

app.textFields.element.tap()

Or double tap on a specific button like this:

app.buttons["Blue"].doubleTap()

There are two more element-specific methods you’ll want to use:

  • For pickers, use adjust(toPickerWheelValue: 1) to make a picker scroll through to select a particular value.
  • For sliders, use adjust(toNormalizedSliderPosition: 0.5) to move it to a specific position,

Typing text

You can activate a text field and type individual letters in the keyboard:

app.textFields.element.tap()
app.keys["t"].tap()
app.keys["e"].tap()
app.keys["s"].tap()
app.keys["t"].tap()

Alternatively, you can select and type whole strings like this:

app.textFields.element.typeText("test")   

Making assertions

Once you’ve found the element you want to test, you can make assertions against it using the regular XCTest functions:

XCTAssertEqual(app.buttons.element.title, "Buy")
XCTAssertEqual(app.staticTexts.element.label, "test")

There are two things that might catch you out. First, percentages for things like progress indicators are reported as strings with “%” attached. So, to check them you might write code like this:

guard let completion = app.progressIndicators.element.value as? String else {
    XCTFail("Unable to find the progress indicator.")
    return
}

XCTAssertEqual(completion, "0%")

Second, checking whether an element exists can be done using the exists check, like this:

XCTAssertTrue(app.alerts["Warning"].exists)

However, it’s usually a good idea to allow a little leeway because you’re working with a real device – animations might be happening, for example. So, instead of using exists it’s common to use waitForExistence(timeout:) instead, like this:

XCTAssertTrue(app.alerts["Warning"].waitForExistence(timeout: 1))

If that element exists immediately then the method returns immediately and the test passes, but if it doesn’t then the method will wait for up to one second – it’s still really fast.

Controlling tests

Creating an instance of XCUIApplication with no parameters lets you control whichever application is specified as the “Target Application” in Xcode’s target settings. So, you can create and launch your target application like this:

XCUIApplication().launch()

You can also create an application with a specific bundle identifier, which is helpful if you have two apps and want to test exchanging data between the two of them:

XCUIApplication(bundleIdentifier: "com.hackingwithswift.hairforceone").launch()

If you want to pass specific arguments to your app, perhaps to make it perform some test-specific set up, you can do that using the launchArguments array:

let app = XCUIApplication()
app.launchArguments = ["enable-testing"]
app.launch()

That will pass “enable-testing” to the app when it’s launched, which it can then respond to, like this:

#if DEBUG
if CommandLine.arguments.contains("enable-testing") {
    configureAppForTesting()
}
#endif

What configureAppForTesting() does is down to you, but you might want to consider running UIView.setAnimationsEnabled(false) to disable animations during testing.

If the system will throw up alerts during your test – for example, if you ask for permission to read the user’s location – you can set a closure to run that can evaluate the system interruption and take any action you want. If you handled the interruption successfully your closure should return true; if not return false, and any other interruption monitors can be run.

For example:

addUIInterruptionMonitor(withDescription: "Automatically allow location permissions") { alert in
    alert.buttons["OK"].tap()
    return true
}

Remember, a failing UI test means your application isn’t in the visual state it expected to be, which in turn means later tests are likely to fail. As a result, it’s common to keep this line inside your setUp() method:

continueAfterFailure = false

Handling screenshots

Xcode automatically takes screenshots as it runs your UI tests, and automatically deletes them if your tests succeed. But if the tests fails then Xcode will keep the screenshots to help you step through visually and figure out what went wrong.

You can also take screenshots by hand when something important has happened, attaching a label of your choosing such as “User authentication” or “Cancelling purchase”, for example. You can also ask Xcode to keep your screenshots even when tests pass, overriding the default behavior.

To create a screenshot, just call screenshot() on any element. That might be a single control in your app, or it might be the whole app itself, in which case you’ll get the whole screen. Once you have that, wrap it in an instance of XCTAttachment, optionally add a name and lifespan, then call add() to add it to your test run.

For example:

let screenshot = app.screenshot()
let attachment = XCTAttachment(screenshot: screenshot)
attachment.name = "My Great Screenshot"
attachment.lifetime = .keepAlways
add(attachment)

XCTAttachment is good for storing things other than screenshots: it has convenience initializers for any Codable and NSCoding objects, or you can just write a Data instance into there.

What now?

Hopefully this has given you a good starting point from which you can write your own UI tests. Previously Xcode’s UI testing system got a pretty bad rap, partly because the recording system was (and is) still fundamentally broken, but partly also because it’s easy to write slow, flaky tests if you don’t pay attention.

So, make sure you prefer waitForExistence(timeout:) over a regular exists check, prefer using firstMatch over element where possible, and try to write the most specific queries you can – it all helps improve speed and help keep your tests working into the future.

 

MASTER SWIFT NOW
Buy Testing Swift Buy Practical iOS 12 Buy Pro Swift Buy Swift Design Patterns Buy Swift Coding Challenges Buy Server-Side Swift (Vapor Edition) Buy Server-Side Swift (Kitura Edition) Buy Hacking with macOS Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with Swift Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

About the author

Paul Hudson is the creator of Hacking with Swift, the most comprehensive series of Swift books in the world. He's also the editor of Swift Developer News, the maintainer of the Swift Knowledge Base, and Mario Kart world champion. OK, so that last part isn't true. If you're curious you can learn more here.

Was this page useful? Let me know!

Average rating: 4.0/5

Click here to visit the Hacking with Swift store >>