Draft page
Access: Prod

XCUITest

XCUITest is Apple’s native UI automation framework for iOS apps. It is built on XCTest, runs from an Xcode UI test target, launches the app under test, queries the accessibility tree, performs user-like interactions, and records failures through Xcode’s test reporting.

This tutorial explains the XCUITest workflow and then maps it to webmate carefully. The repository documents webmate app testing, iOS debugging, app upload, and Appium support; it does not document direct XCUITest execution as a webmate automation capability.

Goal

By the end of this tutorial you will understand how to create an iOS UI test target, launch the app, use accessibility identifiers, wait for UI state, run tests locally and in CI, and use webmate as a supporting environment for app inspection and debugging.

Before You Start

You need a macOS development environment with Xcode, an iOS app project, and a simulator or device that can run the app. The app should expose accessibility identifiers for important controls. Without those identifiers, UI tests often fall back to labels or tree positions that become fragile as the interface changes.

Project Setup

In Xcode, add a UI Testing Bundle target to the app project. Xcode creates a test class that subclasses XCTestCase. A minimal UI test imports XCTest, creates an XCUIApplication, launches it, interacts with elements, and asserts the result.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import XCTest

final class LoginUITests: XCTestCase {
    override func setUpWithError() throws {
        continueAfterFailure = false
    }

    func testUserCanOpenLoginScreen() throws {
        let app = XCUIApplication()
        app.launch()

        app.buttons["login-button"].tap()
        XCTAssertTrue(app.staticTexts["login-title"].waitForExistence(timeout: 5))
    }
}

The strings in brackets are accessibility identifiers or labels exposed by the app. Prefer identifiers for test-only contracts and keep labels for user-facing text assertions.

Step 1: Launch With a Known State

UI tests should control app state explicitly. Use launch arguments and launch environment values to put the app into a predictable mode.

1
2
3
4
let app = XCUIApplication()
app.launchArguments += ["--ui-testing"]
app.launchEnvironment["API_BASE_URL"] = "https://staging.example.com"
app.launch()

The app can read these values at startup and disable animations, select a staging backend, reset local data, or skip onboarding. Keep this behavior limited to test builds or guarded code paths.

Step 2: Add Accessibility Identifiers

XCUITest sees the app through the accessibility tree. Add identifiers to controls that tests need to target.

1
2
3
4
Button("Sign in") {
    signIn()
}
.accessibilityIdentifier("sign-in-button")

Then the test can target the button without relying on its visible text:

1
app.buttons["sign-in-button"].tap()

This is both a testing and accessibility discipline. If a critical control cannot be found clearly by a test, it may also be difficult for assistive technologies or debugging tools to interpret.

Step 3: Wait for UI State

Use waitForExistence(timeout:) instead of fixed sleeps. The test should wait for a specific UI contract.

1
2
let dashboard = app.staticTexts["dashboard-title"]
XCTAssertTrue(dashboard.waitForExistence(timeout: 10))

For asynchronous flows, wait for the state that proves the operation completed: a confirmation message, a new screen title, a disabled loading indicator, or a populated list.

Step 4: Structure Tests and Helpers

As the suite grows, move repeated interactions into small screen objects. Keep assertions in tests unless the helper represents a clear reusable contract.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
struct LoginScreen {
    let app: XCUIApplication

    func signIn(email: String, password: String) {
        app.textFields["email-field"].tap()
        app.textFields["email-field"].typeText(email)
        app.secureTextFields["password-field"].tap()
        app.secureTextFields["password-field"].typeText(password)
        app.buttons["sign-in-button"].tap()
    }
}

Avoid helpers that hide too much. A failed test should still read like a user journey.

Step 5: Use Test Plans

Xcode test plans help define configurations, environment variables, arguments, and test selections. Use them to separate smoke tests, release checks, and environment-specific runs.

Common test-plan variants include a fast simulator smoke plan, a staging-backend regression plan, and a release-candidate plan. Keep the pull-request plan small and reliable. Run broader device and OS coverage on a schedule.

Step 6: Run in CI

CI commonly runs XCUITest through xcodebuild:

1
2
3
4
xcodebuild test \
  -scheme ExampleApp \
  -testPlan SmokeUITests \
  -destination 'platform=iOS Simulator,name=iPhone 15'

Preserve the result bundle, screenshots, and logs as CI artifacts. UI failures are difficult to diagnose from a single assertion message.

How This Maps to webmate

webmate documentation in this repository covers app upload, iOS debugging, interactive testing, and Appium-based automation. It does not define a direct XCUITest execution path. Use XCUITest in the Apple tooling stack when native iOS UI automation is required.

webmate can still support the quality workflow around XCUITest. You can upload and inspect builds, reproduce failures manually on iOS devices, use Workbench-style debugging and screenshots, and compare observed behavior with what the native test reports. If the requirement is remote automated execution through webmate, use the documented Appium path instead.

Debugging

If an element cannot be found, inspect the accessibility identifier and confirm the app is on the expected screen. If a tap fails, check whether an alert, permission prompt, keyboard, or animation blocks the control. If a test passes alone but fails in a suite, look for shared state, persistent data, or launch arguments that are not reset.

Use continueAfterFailure = false for journey tests so the first broken step is the failure you inspect. Continuing after a missing login button usually produces confusing later failures.

Verify the Result

A good XCUITest setup can launch the app with a known state, target controls by accessibility identifier, wait for visible results, run through xcodebuild, and preserve failure artifacts. It should be clear which failures belong to the app, which belong to test data, and which belong to device or simulator setup.

Next Steps

Use Debugging iOS App and Upload App for related webmate workflows. Use Appium when you need WebDriver-based mobile automation that maps directly to documented webmate execution.