| name | xcuitest-patterns |
| description | XCUITest best practices, patterns, and anti-patterns for Swift/SwiftUI apps. Use when writing UI tests, debugging test failures, reviewing UI test quality, or encountering 'element not found' errors in XCUITest. |
XCUITest Patterns & Best Practices
Element Queries
Preferred: Accessibility Identifier
app.buttons["settings_button_save"].tap()
app.otherElements["screen_home"].waitForExistence(timeout: 5)
app.toggles["settings_toggle_darkMode"].tap()
Fallback: Element Type + Predicate
let cell = app.cells.containing(.staticText, identifier: "WiFi Network").firstMatch
let alert = app.alerts.firstMatch
Avoid: Index-Based Queries
app.buttons.element(boundBy: 2).tap()
app.buttons["garden_button_addPlant"].tap()
Wait Patterns
Always Wait for Elements
let element = app.otherElements["screen_garden"]
XCTAssertTrue(element.waitForExistence(timeout: 10), "Garden screen should appear")
XCTAssertTrue(app.otherElements["screen_garden"].exists)
sleep(3)
Wait for Element to Disappear
let spinner = app.activityIndicators.firstMatch
let disappeared = NSPredicate(format: "exists == false")
expectation(for: disappeared, evaluatedWith: spinner)
waitForExpectations(timeout: 30)
Wait for Element Value/State
let label = app.staticTexts["home_label_taskCount"]
let hasValue = NSPredicate(format: "label != 'Loading...'")
expectation(for: hasValue, evaluatedWith: label)
waitForExpectations(timeout: 30)
Interaction Patterns
Scrolling to Find Elements
func scrollToElement(_ element: XCUIElement, in scrollView: XCUIElement, maxSwipes: Int = 5) {
var swipes = 0
while !element.isHittable && swipes < maxSwipes {
scrollView.swipeUp()
swipes += 1
}
}
let card = app.otherElements["garden_card_plantHealth"]
scrollToElement(card, in: app.scrollViews.firstMatch)
card.tap()
Text Input
let field = app.textFields["garden_textfield_plantName"]
XCTAssertTrue(field.waitForExistence(timeout: 5))
field.tap()
field.clearAndTypeText("Basil")
extension XCUIElement {
func clearAndTypeText(_ text: String) {
guard let currentValue = self.value as? String, !currentValue.isEmpty else {
self.typeText(text)
return
}
let deleteString = String(repeating: XCUIKeyboardKey.delete.rawValue, count: currentValue.count)
self.typeText(deleteString)
self.typeText(text)
}
}
Picker Selection
app.pickerWheels.firstMatch.adjust(toPickerWheelValue: "Weekly")
app.buttons["settings_picker_theme"].tap()
app.buttons["Dark"].tap()
Toggle Verification
let toggle = app.switches["settings_toggle_notifications"]
XCTAssertTrue(toggle.waitForExistence(timeout: 5))
let initialValue = toggle.value as? String
toggle.tap()
let newValue = toggle.value as? String
XCTAssertNotEqual(initialValue, newValue, "Toggle should change state")
Test Organization
One Concern Per Test
func testGarden_tapAddPlant_showsAddPlantSheet() {
navigateToTab("Garden")
app.buttons["garden_button_addPlant"].tap()
XCTAssertTrue(app.otherElements["screen_addPlant"].waitForExistence(timeout: 5),
"Add plant sheet should appear after tapping add button")
}
func testGardenScreen() {
}
Relaunch for Persistence Tests
func testSettings_darkModeToggle_persistsAcrossRelaunch() {
navigateToTab("Settings")
let toggle = app.switches["settings_toggle_darkMode"]
toggle.tap()
let valueAfterToggle = toggle.value as? String
app.terminate()
app.launch()
navigateToTab("Settings")
let toggleAfterRelaunch = app.switches["settings_toggle_darkMode"]
XCTAssertTrue(toggleAfterRelaunch.waitForExistence(timeout: 5))
XCTAssertEqual(toggleAfterRelaunch.value as? String, valueAfterToggle,
"Dark mode setting should persist across app relaunch")
}
Screenshots for Debugging
func testHome_loadedState_showsAllCards() {
let home = app.otherElements["screen_home"]
XCTAssertTrue(home.waitForExistence(timeout: 10))
takeScreenshot("home-loaded")
XCTAssertTrue(app.otherElements["home_card_todayTasks"].exists)
XCTAssertTrue(app.otherElements["home_card_weather"].exists)
}
Anti-Patterns
Testing Implementation Details
app.scrollViews.otherElements.buttons.matching(identifier: "").element(boundBy: 0)
app.buttons["home_button_refresh"]
Asserting Exact Dynamic Values
XCTAssertEqual(label.label, "3 plants need watering")
XCTAssertTrue(label.label.contains("watering"), "Should mention watering")
XCTAssertFalse(label.label.isEmpty, "Task label should not be empty")
Chaining Navigation Without Verification
app.tabBars.buttons["Garden"].tap()
app.otherElements["garden_row_plant_1"].tap()
app.buttons["plantDetail_button_water"].tap()
navigateToTab("Garden")
let row = app.otherElements["garden_row_plant_1"]
XCTAssertTrue(row.waitForExistence(timeout: 5))
row.tap()
XCTAssertTrue(app.otherElements["screen_plantDetail"].waitForExistence(timeout: 5))
app.buttons["plantDetail_button_water"].tap()
Ignoring Simulator Limitations
func testPlantScanner_identifiesPlant() { ... }
func testPlantScanner_showsFallbackOnSimulator() {
navigateToTab("Garden")
app.buttons["garden_button_scanPlant"].tap()
XCTAssertTrue(app.staticTexts.matching(
NSPredicate(format: "label CONTAINS 'not available'")
).firstMatch.waitForExistence(timeout: 5))
}
macOS-Specific Patterns
Menu Bar Testing
let menuBar = app.menuBarItems["Cultivation"]
menuBar.click()
app.menuItems["Preferences..."].click()
Window Management
let window = app.windows.firstMatch
window.typeKey("f", modifierFlags: [.command, .control])
Toolbar Items
app.toolbarButtons["toolbar_button_share"].click()
Launch Arguments for Test Control
app.launchArguments = ["--uitesting"]
if ProcessInfo.processInfo.arguments.contains("--uitesting") {
}
Common launch arguments:
--uitesting — general test mode flag
--skip-onboarding — bypass first-launch flow
--mock-data — use fixture data instead of real network
--disable-animations — speed up tests
--reset-state — clear UserDefaults/persisted state