XCUIElement – Discovery

In XCUITest automation, I arrange test code in using a Screen Object model pattern. This is based on the web’s Page Object model. First, there is a group of classes which model screens on an app. Second, a group of classes contain test case functions that rely on the screen classes for instances of screen elements and functions that perform actions on the screen in use for the test. Elements are modeled by the class named XCUIElement.

A screen class contains references to related screens, queries for screen elements and common functions. Common functions include simple wrappers like getLoginButton() which return an element query and getRow(atIndex: Int) which returns row at an index from a table.

Skeletal Screen Object Example

class DemoScreen {
    var app:                XCUIApplication
    var openSafariButton:   XCUIElement

    init(testApp: XCUIApplication) {
        self.app = testApp
        openSafariButton = app.buttons[“safari-button”]
    }
}

In the example above, the bare minimum code is shown for the sort of screen class I frequently implement for my XUCITest automation projects. Below, I illustrate the portions of the class.

var app:    XCUIApplication

This instance variable is key as it is the root element for the screen representing this class. Queries for elements will take advantage of this variable. The variables at the top of the class are not defined as constants since they must get initialized in the init() function.

var openSafariButton:   XCUIElement

This is a declaration for an element on the screen. An element is modeled by the XCUIElement class.

init(testApp: XCUIApplication) {
    self.app = testApp
    openSafariButton = app.buttons[“safari-button”]
}

The init() function is required so that class variables get initialized. The right side of the element assignment is an XCUIElementQuery which uses the variable named app. The query relies on a unique identifier for a button.

My recommendation is to always use a unique identifier wherever possible. This guards against UI changes that break the query and do not invalidate the UI element. If an accessibility identifier is unavailable for an element then there are ways to add one. I will explore these ways in a future post.

XCUIElementQuery

There are several ways that can be used to query for an element on the screen. First, the data source for identifying elements is an element tree. In the test function, testSafariButton(), a line with print(app.debugDescription)will print the output shown second in the test log.

Skeletal Test Class

import XCTest

class DemoAutoUITests: XCTestCase {

    // MARK: - Properties
    let app = XCUIApplication()

    var demoScreen: DemoScreen!

    let safariApp = XCUIApplication(bundleIdentifier: "com.apple.mobilesafari")

    // MARK: - Lifecycle
    override func setUp() {
        super.setUp()
        demoScreen = DemoScreen(testApp: app)

        continueAfterFailure = false
        app.launch()
    }

    override func tearDown() {
        super.tearDown()
    }

    // MARK: - Tests
    func testOpenSafari() {
        let timeout = 15.0

        // prints the Element Tree
        print(app.debugDescription)

        demoScreen.openSafariButton.tap()
        XCTAssertTrue(safariApp.wait(for: .runningForeground, timeout: timeout), "Safari app is not in foreground")
    }
}

Element Tree Logged by print(app.debugDescription

Attributes: Application, pid: 83644, label: 'DemoAuto'
Element subtree:
 →Application, 0x600001da08f0, pid: 83644, label: 'DemoAuto'
    Window (Main), 0x600001da0750, {{0.0, 0.0}, {414.0, 896.0}}
      Other, 0x600001da05b0, {{0.0, 0.0}, {414.0, 896.0}}
        Button, 0x600001da0820, {{22.0, 80.0}, {136.0, 30.0}}, identifier: 'show-text-button', label: 'Show Label'
        StaticText, 0x600001da0a90, {{22.0, 129.0}, {136.0, 21.0}}, identifier: 'show-text-label'
        Button, 0x600001da0b60, {{22.0, 226.0}, {136.0, 30.0}}, label: 'Open Settings'
        Button, 0x600001da0c30, {{22.0, 264.0}, {136.0, 30.0}}, identifier: 'open-safari-button', label: 'Open Safari'
    Window, 0x600001da0d00, {{0.0, 0.0}, {414.0, 896.0}}
      StatusBar, 0x600001da0dd0, {{0.0, 0.0}, {414.0, 44.0}}
        Other, 0x600001da0ea0, {{0.0, 0.0}, {414.0, 44.0}}
          Other, 0x600001da0f70, {{0.0, 0.0}, {414.0, 44.0}}
            Other, 0x600001da1040, {{8.0, -4.0}, {197.0, 11.5}}
            Other, 0x600001da1110, {{210.0, -4.0}, {197.0, 11.5}}
            Other, 0x600001da11e0, {{8.0, 19.0}, {197.0, 11.5}}
            Other, 0x600001da12b0, {{16.5, 14.5}, {70.5, 16.0}}
              StaticText, 0x600001da1380, {{34.5, 15.5}, {34.5, 19.5}}, label: '7:45 AM'
            Other, 0x600001da1450, {{23.0, 10.0}, {61.0, 22.0}}
            Other, 0x600001da1520, {{210.0, 19.0}, {197.0, 11.5}}
            Other, 0x600001da15f0, {{326.5, 14.5}, {70.5, 16.0}}
              Other, 0x600001da16c0, {{326.5, 19.0}, {18.5, 11.5}}, label: 'Cellular', value: No signal
              Other, 0x600001da1790, {{349.5, 19.0}, {16.5, 11.5}}, identifier: '3 of 3 Wi-Fi bars', value: SSID, 3 of 3 Wi-Fi...
              Other, 0x600001da1860, {{370.5, 18.5}, {26.5, 12.5}}, label: '98% battery power', value: Charging
            Other, 0x600001da1930, {{326.5, 14.5}, {70.5, 16.0}}
          Other, 0x600001da1a00, {{8.5, 24.5}, {81.5, 13.5}}
Path to element:
 →Application, pid: 83644, label: 'DemoAuto'
Query chain:
 →Find: Target Application 'com.joeferrara.DemoAuto'
  Output: {
    Application, pid: 83644, label: 'DemoAuto'
  }

An easy way to see the element tree from Xcode is to set a breakpoint in your code after the line which prints the debugDescription property.

Test at breakpoint right after printing the element tree to the system console

Using data derived from the element tree, we can see identifiers, labels and positions of elements on the screen. In this example we have three buttons and one label (a label here is a staticText).

Shown below are three of the ways we can locate an element using an instance of XCUIElementQuery.

        // Located by Accessibility Identifier
        labelStaticText = app.staticTexts["show-text-label"].firstMatch
    
        // Located by Label
        showLabelButton = app.buttons["Show Label"].firstMatch

        // Located by Position
        let secondButtonOnStoryBoard = 1
        openSettingsButton = app.buttons.element(boundBy: secondButtonOnStoryBoard)

I prefer to use an accessibility identifier when querying for an element. Identifiers are most often unique to a screen and have less of a chance of changing than other methods. A label can easily be refined from release to release and a screen position can change if either the external design changes or internal code makes it so that the order of elements changes.

When we can guarantee that there is only one element on the screen with a particular identifier then it is beneficial to use the firstMatch property so that the query engine stops searching for matches on the query.

There are additional functions which are used to match elements including use of direct children elements, descendants, match by identifier, plus ways to identify an element with wildcard matching.

Ways to Learn about Screen Elements

I have shown one way to find the screen elements using print(app.debugDescription)in test code. Below is a list of other ways to find the elements on a screen. I plan to get into more detail for some of these methods in future blog posts.

  • Xcode Test Recorder
  • Include po print(app.debugDescription)in a test function
  • Xcode debugger command – po print ( app.debugDescription )
  • Examine story board or XIB resource for a screen
  • Xcode Accessibility Inspector

Thank you for reading. I welcome your feedback and comments for this post.



Actionable UI Test Assertions

When writing your XCUITest UI test, it is important to provide enough data when an assertion fails so that someone looking at a test run report can most easily see what failed and begin to figure out the “why”. The context for this post is for tests run either within Xcode or through Xcode’s Bots tool. That way, you have the graphical report to examine.

Assertions

The most basic thing I look for is that the assertion message is included with an assertion statement.

You could syntactically be okay with something like this :

XCTAssertTrue( loginButton.exists )

However, that assertion will only show up as a failure in a report with no context other than a line number. An improved message looks like this :

XCTAssertTrue( loginButton.exists, "The login button does not exist" )

Attachments

A useful piece of data to have is to show the tree of elements present when a failure occurs. You can easily log this information as in :

print ( XCUIApplication().debugDescription )

When a test report is created in Xcode, you can attach things like screenshots, text documents and other things to the report. I use it for including a debug description of the last screen used when an assertion fails. This can help with analysis of the test failure. This is in addition to the screen shots included with the Bot report by default.

An example block of code to include in your tearDown() method for a test case :

XCTContext.runActivity(named: "Create Debug Description") { _ in
    let elementsTree = app.debugDescription
    let attachment = XCTAttachment(string: elementsTree)
    attachment.name = "Final Screen Debug Description"
    add(attachment)
}

Example showing what a Bot report looks like when the UI Test cannot find an element.

Here is the Final Screen Debug Description element tree attachment looks like from above example.

While you can have a suite of UI Tests that take detailed actions and numerous assertions, they are most effective when someone can interpret the results and get a head start on understanding the root cause of a test failure.

When working with a team, it is best to get the test results recorded plus notifying team members about the failure so that it can be acted on right away.