UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

Xcode UI Testing Cheat Sheet

The least you need to know to make XCTest work with user interfaces

Paul Hudson       @twostraws

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

Instead, user interface tests access your app using iOS’s accessibility system: they scan the interface for whatever user interface controls you request, manipulate them to follow instructions you’ve provided, then make assertions about the end state of your app. This makes them much more fragile than unit tests because small 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 common problems using Xcode’s UI testing system.

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

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 user interface. While the queries above are fine when you’re just getting started, there are lots of 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 significantly 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 – user interface 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 ways to 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?

I hope this has given you a good starting point from which you can write your own user interface 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.

If you have any tips and tricks for UI testing that aren't covered here, send me a tweet and I'll add them!

Hacking with Swift is sponsored by Essential Developer

SPONSORED Join a FREE crash course for mid/senior iOS devs who want to achieve an expert level of technical and practical skills – it’s the fast track to being a complete senior developer! Hurry up because it'll be available only until April 28th.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

BUY OUR BOOKS
Buy Pro Swift Buy Pro SwiftUI Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Beyond Code

Was this page useful? Let us know!

Average rating: 4.8/5

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.