UI Testing
Overview
Wait for conditions, not arbitrary timeouts. Core principle Flaky tests come from guessing how long operations take. Condition-based waiting eliminates race conditions.
NEW in WWDC 2025: Recording UI Automation allows you to record interactions, replay across devices/languages, and review video recordings of test runs.
Example Prompts
These are real questions developers ask that this skill is designed to answer:
1. "My UI tests pass locally on my Mac but fail in CI. How do I make them more reliable?"
β The skill shows condition-based waiting patterns that work across devices/speeds, eliminating CI timing differences
2. "My tests use sleep(2) and sleep(5) but they're still flaky. How do I replace arbitrary timeouts with real conditions?"
β The skill demonstrates waitForExistence, XCTestExpectation, and polling patterns for data loads, network requests, and animations
3. "I just recorded a test using Xcode 26's Recording UI Automation. How do I review the video and debug failures?"
β The skill covers Video Debugging workflows to analyze recordings and find the exact step where tests fail
4. "My test is failing on iPad but passing on iPhone. How do I write tests that work across all device sizes?"
β The skill explains multi-factor testing strategies and device-independent predicates for robust cross-device testing
5. "I want to write tests that are not flaky. What are the critical patterns I need to know?"
β The skill provides condition-based waiting templates, accessibility-first patterns, and the decision tree for reliable test architecture
Red Flags β Test Reliability Issues
If you see ANY of these, suspect timing issues:
- Tests pass locally, fail in CI (timing differences)
- Tests sometimes pass, sometimes fail (race conditions)
- Tests use
sleep() or Thread.sleep() (arbitrary delays)
- Tests fail with "UI element not found" then pass on retry
- Long test runs (waiting for worst-case scenarios)
Quick Decision Tree
Test failing?
ββ Element not found?
β ββ Use waitForExistence(timeout:) not sleep()
ββ Passes locally, fails CI?
β ββ Replace sleep() with condition polling
ββ Animation causing issues?
β ββ Wait for animation completion, don't disable
ββ Network request timing?
ββ Use XCTestExpectation or waitForExistence
Core Pattern: Condition-Based Waiting
β WRONG (Arbitrary Timeout):
func testButtonAppears() {
app.buttons["Login"].tap()
sleep(2)
XCTAssertTrue(app.buttons["Dashboard"].exists)
}
β
CORRECT (Wait for Condition):
func testButtonAppears() {
app.buttons["Login"].tap()
let dashboard = app.buttons["Dashboard"]
XCTAssertTrue(dashboard.waitForExistence(timeout: 5))
}
Common UI Testing Patterns
Pattern 1: Waiting for Elements
func waitForElement(_ element: XCUIElement, timeout: TimeInterval = 5) -> Bool {
return element.waitForExistence(timeout: timeout)
}
XCTAssertTrue(waitForElement(app.buttons["Submit"]))
Pattern 2: Waiting for Element to Disappear
func waitForElementToDisappear(_ element: XCUIElement, timeout: TimeInterval = 5) -> Bool {
let predicate = NSPredicate(format: "exists == false")
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: element)
let result = XCTWaiter().wait(for: [expectation], timeout: timeout)
return result == .completed
}
XCTAssertTrue(waitForElementToDisappear(app.activityIndicators["Loading"]))
Pattern 3: Waiting for Specific State
func waitForButton(_ button: XCUIElement, toBeEnabled enabled: Bool, timeout: TimeInterval = 5) -> Bool {
let predicate = NSPredicate(format: "isEnabled == %@", NSNumber(value: enabled))
let expectation = XCTNSPredicateExpectation(predicate: predicate, object: button)
let result = XCTWaiter().wait(for: [expectation], timeout: timeout)
return result == .completed
}
let submitButton = app.buttons["Submit"]
XCTAssertTrue(waitForButton(submitButton, toBeEnabled: true))
submitButton.tap()
Pattern 4: Accessibility Identifiers
Set in app:
Button("Submit") {
}
.accessibilityIdentifier("submitButton")
Use in tests:
func testSubmitButton() {
let submitButton = app.buttons["submitButton"]
XCTAssertTrue(submitButton.waitForExistence(timeout: 5))
submitButton.tap()
}
Why: Accessibility identifiers don't change with localization, remain stable across UI updates.
Pattern 5: Network Request Delays
func testDataLoads() {
app.buttons["Refresh"].tap()
let loadingIndicator = app.activityIndicators["Loading"]
XCTAssertTrue(waitForElementToDisappear(loadingIndicator, timeout: 10))
XCTAssertTrue(app.cells.count > 0)
}
Pattern 6: Animation Handling
func testAnimatedTransition() {
app.buttons["Next"].tap()
let destinationView = app.otherElements["DestinationView"]
XCTAssertTrue(destinationView.waitForExistence(timeout: 2))
RunLoop.current.run(until: Date(timeIntervalSinceNow: 0.3))
}
Testing Checklist
Before Writing Tests
When Writing Tests
After Writing Tests