Authors XCUIest UI tests for iOS / iPadOS / tvOS - uses the three-class XCUIApplication / XCUIElement / XCUIElementQuery pattern, sets accessibility identifiers on production code, runs via `xcodebuild test` with destination, parses the `xcresult` bundle. Use when an iOS app needs UI tests in Apple's first-party framework (no external runtime; native to Xcode).
77
97%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
XCUITest is Apple's first-party UI testing framework, integrated with Xcode and built on the XCTest framework (xcui-fundamentals).
"UI testing in Xcode is built on three fundamental classes: XCUIApplication, XCUIElement, XCUIElementQuery." (xcui-fundamentals)
The general pattern: Query → Synthesize → Assert.
"1. Query - Use XCUIElementQuery to find an XCUIElement 2. Synthesize - Synthesize an event and send it to the XCUIElement 3. Assert - Use an assertion to compare the element's state against expected reference state" (xcui-fundamentals)
If the app is React Native, see
detox-testing. For cross-platform
Appium-style coverage, see
appium-testing.
In Xcode: File → New → Target → UI Testing Bundle. The template
generates a .swift test class with a default setUp:
import XCTest
final class CartUITests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = false // critical (default)
XCUIApplication().launch()
}
}Per xcui-fundamentals:
"
continueAfterFailure = NOis the default (recommended) because UI test steps are dependent on previous steps."
XCUITest finds elements via the accessibility tree. Hard-coded labels / text are brittle - set explicit identifiers in the SUT:
// Production code
let placeOrderButton = UIButton()
placeOrderButton.accessibilityIdentifier = "place-order-button"Then in tests:
let app = XCUIApplication()
app.buttons["place-order-button"].tap()accessibilityIdentifier (not accessibilityLabel) - labels are
user-facing and translated; identifiers are dev-only and stable.
let app = XCUIApplication()
// By accessibility identifier (preferred)
app.buttons["place-order-button"].tap()
app.textFields["email-field"].typeText("user@example.com")
// By type + text
app.staticTexts["Welcome"].swipeUp()
// Predicate-based query
let cells = app.tables.cells.matching(
NSPredicate(format: "label CONTAINS[c] 'BOOK-001'")
)
cells.element(boundBy: 0).tap()
// Wait for an element
XCTAssert(app.staticTexts["Order confirmed"].waitForExistence(timeout: 5))// Tap
app.buttons["submit"].tap()
// Type
app.textFields["email"].typeText("user@example.com")
// Swipe / drag
app.cells.element(boundBy: 0).swipeLeft()
// Pinch
app.images["map"].pinch(withScale: 2.0, velocity: 1.0)
// Press for duration (long-press)
app.buttons["context-menu"].press(forDuration: 1.0)
// System keyboard return
app.keyboards.buttons["return"].tap()let confirmation = app.staticTexts["Order confirmed"]
XCTAssertTrue(confirmation.exists)
XCTAssertEqual(app.staticTexts["order-id"].label, "ORD-12345")
XCTAssertTrue(app.buttons["submit"].isEnabled)exists returns immediately; waitForExistence(timeout:) waits
up to N seconds. Use waitForExistence for any post-tap state
that depends on async work.
# From the project directory:
xcodebuild test \
-project MyApp.xcodeproj \
-scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 15,OS=latest' \
-resultBundlePath TestResults.xcresultPer-destination patterns:
| Use | Destination string |
|---|---|
| Latest sim | 'platform=iOS Simulator,name=iPhone 15,OS=latest' |
| Specific OS | 'platform=iOS Simulator,name=iPhone 14,OS=17.4' |
| Connected device | 'platform=iOS,id=<UDID>' |
| Multi-device matrix | Pass -destination multiple times. |
.xcresultThe result bundle is binary. Extract via xcresulttool:
xcrun xcresulttool get test-results summary --path TestResults.xcresult --format json > results.jsonThen use junit-xml-analysis (in the qa-test-reporting plugin)
on the JUnit-equivalent shape (or directly on the JSON for richer
data).
# .github/workflows/ios-tests.yml
jobs:
ui-tests:
runs-on: macos-15
steps:
- uses: actions/checkout@v5
- run: xcodebuild test -project MyApp.xcodeproj -scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 15,OS=latest' \
-resultBundlePath TestResults.xcresult
- uses: actions/upload-artifact@v4
if: always()
with:
name: xcresult
path: TestResults.xcresultGitHub Actions provides macos-15 runners with Xcode pre-installed.
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Querying by accessibilityLabel | Labels are translated; tests fail in non-English locales. | Use accessibilityIdentifier (Step 2-3). |
Thread.sleep for async waits | Flaky on slow runners; slow on fast. | waitForExistence(timeout:) (Step 5). |
continueAfterFailure = true | One failure cascades into N false positives. | Default false (Step 1). |
| Hard-coded text in queries | Copy changes break tests. | accessibility identifiers (Step 2). |
| Running against wrong destination ("latest" without pin) | Test passes on Xcode 16, fails on 15; reproducibility issues. | Pin OS version explicitly in CI. |
Skipping xcresult upload | Failure debugging needs the screenshots / videos in the bundle. | Always upload (Step 8). |
percy-visual-regression-testing (in the qa-visual-regression plugin)
or screenshot-comparison helpers.continueAfterFailure = NO default.espresso-suite - Android sibling.appium-testing - cross-platform
alternative when iOS + Android share tests.junit-xml-analysis - downstream parser for JUnit-converted xcresult.