XCUIApplication & Element Queries
What is XCUITest?
XCUITest is Apple UI testing framework that launches your app in a simulator or device and interacts with it like a real user. Unlike unit tests that test individual components, UI tests verify entire user journeys from tap to screen change.
Setting Up a UI Test Target
Add a UI Testing Bundle to your project via File, New, Target. The default test file looks like:
import XCTest
class MyAppUITests: XCTestCase {
let app = XCUIApplication()
override func setUpWithError() throws {
continueAfterFailure = false
app.launch()
}
func testLoginFlow() throws {
let emailField = app.textFields["emailTextField"]
emailField.tap()
emailField.typeText("user@example.com")
let passwordField = app.secureTextFields["passwordTextField"]
passwordField.tap()
passwordField.typeText("password123")
app.buttons["loginButton"].tap()
XCTAssertTrue(app.staticTexts["Welcome"].waitForExistence(timeout: 5))
}
}
XCUIApplication
XCUIApplication represents your app as a whole. Key methods include app.launch() to start the app, app.launchEnvironment to set environment variables, and app.launchArguments for command-line arguments.
Element Queries
Find elements using accessibility identifiers, labels, or type:
let button = app.buttons["submitButton"]
let label = app.staticTexts["Welcome back"]
let textField = app.textFields.firstMatch
let cell = app.tables.cells.containing(.staticText, identifier: "Item 1").element
let item = app.collectionViews.cells.element(boundBy: 0)
Accessibility Identifiers
Set accessibility identifiers in SwiftUI or UIKit to make UI tests reliable:
// SwiftUI
Button("Submit") { }
.accessibilityIdentifier("submitButton")
// UIKit
submitButton.accessibilityIdentifier = "submitButton"
Always use accessibility identifiers rather than relying on button text which may change with localization.
Element Interaction & Waiting
Interacting with Elements
XCUITest provides methods to simulate real user interactions:
downloadButton.tap()
searchField.tap()
searchField.typeText("SwiftUI tutorial")
app.swipeLeft()
app.swipeUp()
cell.press(forDuration: 2.0)
app.orientation = .landscapeLeft
app.pinch(withScale: 2.0, velocity: 1.0)
Wait Strategies
UI tests must wait for animations, network calls, and screen transitions:
// Wait for element existence
let alert = app.alerts["Error"]
XCTAssertTrue(alert.waitForExistence(timeout: 10))
// Wait for element to disappear
let spinner = app.activityIndicators["loading"]
XCTAssertFalse(spinner.waitForExistence(timeout: 5))
// Wait for hittable (ready for interaction)
let payButton = app.buttons["Pay"]
let isHittable = payButton.waitForExistence(timeout: 5) && payButton.isHittable
XCTAssertTrue(isHittable)
Handling Alerts and Sheets
func testDeleteItem_showsConfirmation() {
app.tables.cells.element(boundBy: 0).swipeLeft()
app.buttons["Delete"].tap()
let alert = app.alerts["Confirm Delete"]
XCTAssertTrue(alert.waitForExistence(timeout: 2))
alert.buttons["Cancel"].tap()
XCTAssertFalse(alert.waitForExistence(timeout: 1))
}
Page Object Pattern
Organize UI tests using the Page Object pattern to reduce duplication:
class LoginPage {
let app: XCUIApplication
let emailField: XCUIElement
let passwordField: XCUIElement
let loginButton: XCUIElement
init(app: XCUIApplication) {
self.app = app
self.emailField = app.textFields["emailField"]
self.passwordField = app.secureTextFields["passwordField"]
self.loginButton = app.buttons["loginButton"]
}
@discardableResult
func login(email: String, password: String) -> HomePage {
emailField.tap()
emailField.typeText(email)
passwordField.tap()
passwordField.typeText(password)
loginButton.tap()
return HomePage(app: app)
}
}
This makes tests readable and maintainable across the project.
Recording & Debugging UI Tests
Xcode Test Recorder
Xcode can record your interactions and generate test code automatically. Open your UI test file, place your cursor inside a test method, click the red Record button, interact with the app in the simulator, then click Stop to see the generated code.
The recorder generates code like:
func testRecordedFlow() {
let app = XCUIApplication()
app.launch()
app.textFields["searchField"].tap()
app.textFields["searchField"].typeText("hello")
app.buttons["searchButton"].tap()
}
The recorder is a starting point. Always refine the generated code with proper waits, assertions, and accessibility identifiers.
Debugging Failed Tests
When a UI test fails, Xcode shows the exact line where failure occurred, a screenshot of the app state at failure, and the view hierarchy. To debug, set a breakpoint in the test, use po app in the console to inspect the element hierarchy, use the Debug View Hierarchy button, and add XCUIDevice.shared.press(.home) to navigate between screens.
Screenshot Attachment
Attach screenshots to test results for debugging:
func testCaptureScreenshot() {
let screenshot = XCUIScreen.main.screenshot()
let attachment = XCTAttachment(screenshot: screenshot)
attachment.name = "Login Screen"
attachment.lifetime = .keepAlways
add(attachment)
}
Test Plans
Use Test Plans to organize test execution. Create a Test Plan via File, New, File, Test Plan. Add test targets and specify which tests to include. Configure parallel execution and test repetitions. Set environment variables for different test configurations.
Accessibility Inspector for Testing
Use Xcode Accessibility Inspector to verify accessibility identifiers are set correctly, check that elements are accessible to VoiceOver, identify missing labels or hints, and debug element hierarchy issues in UI tests.
Quiz
1. What does continueAfterFailure = false do in a UI test?
2. How do you wait for an element to appear in XCUITest?
3. Why should you use accessibility identifiers instead of button labels?
4. What is the Page Object pattern used for in UI testing?
Flashcards
Question
What is the difference between XCUIApplication and XCUIElement?
Click to reveal answer
Answer
XCUIApplication represents the entire app and is used to launch it. XCUIElement represents a single UI element like a button or text field that you interact with.
Question
What does app.launchEnvironment do?
Click to reveal answer
Answer
Sets environment variables that the app can read at launch time, useful for configuring mock APIs or skipping onboarding in tests.
Question
How do you handle system alerts in UI tests?
Click to reveal answer
Answer
Use app.alerts["title"].buttons["Button"].tap() to interact with system alerts like permission dialogs.
Revision Notes
Key Takeaways
- 1. Always set accessibility identifiers for testable elements
- 2. Use waitForExistence instead of Thread.sleep
- 3. Set continueAfterFailure = false for cleaner debugging
- 4. Apply Page Object pattern for maintainable tests
- 5. Use launchEnvironment to configure test-specific behavior
Interview Tips
- • Explain the difference between unit tests and UI tests
- • Describe how you handle flaky UI tests with proper waits
- • Discuss the Page Object pattern and its benefits
- • Talk about how you would test a complex multi-screen flow
Cheat Sheet
XCUITest Quick Reference
- XCUIApplication() represents your app
- app.launch() starts the app
- app.buttons["id"].tap() to interact
- waitForExistence(timeout:) to wait
- continueAfterFailure = false to stop on first failure
- .accessibilityIdentifier() to tag elements