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.

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.