Browse the Knowledge Hub211 resources
XCUITest review: iOS UI testing tutorial and pitfalls
XCUITest is the common name for Apple’s UI testing support: tests written in Swift or Objective-C with XCTest, driving the app through the XCUIAutomation framework from a separate test runner. It ships free with Xcode and covers iOS, iPadOS, macOS, tvOS and watchOS apps. Xcode 27, current since 14 September 2026, is the version reviewed here.
QAble has evaluated this tool and works alongside it. The factual sections are complete; our view is bounded to what we have actually seen rather than a long-term verdict.
- last reviewed
- 13 October 2026
- version covered
- Xcode 27 (XCTest and XCUIAutomation)
- licence
- Freemium
At a glance
XCUITest at a glance
- Vendor
- Apple
- Category
- Mobile automation
- Licence
- Freemium (Proprietary, free with Xcode)
- Platforms
- iOS, iPadOS, macOS, tvOS, watchOS
- Languages
- Swift, Objective-C
- Integrates with
- Xcode, Xcode Cloud, xcodebuild
- First released
- 2015
- Version reviewed
- Xcode 27 (XCTest and XCUIAutomation)
Verdict
Our verdict on XCUITest
Strengths
- Free with Xcode, with no extra install, server or driver between tests and the app
- Recording in Xcode writes element queries for you and offers alternative queries per element
- Can interact with system alerts, Settings and other installed apps from the same test
- Built-in accessibility audits, screenshots, activities and result bundles that open in Xcode
- Parallel runs across simulator clones, plus retries and repetitions from xcodebuild
Weaknesses
- Apple platforms only, and every build and run needs a Mac with Xcode
- Tests drive the app from a separate process and cannot reach its code or state directly
- Queries depend on accessibility data; missing identifiers mean brittle label or index queries
- UI tests must use XCTest; Swift Testing does not cover them
- An Xcode or iOS release can change behaviour, and CI images lag new Xcode versions
Our take
XCUITest is the natural choice when an iOS team writes its own UI tests in Swift and keeps them beside the app: no extra install, first-class Xcode support, and the same framework that Appium’s iOS driver drives underneath, without the extra layers. QAble has evaluated it rather than maintained a long-running suite on it, so treat this as a bounded take. Expect three costs. It needs Macs everywhere, including CI. Tests see only what the accessibility layer exposes, so stable accessibility identifiers are a prerequisite you agree with developers. And UI tests remain an XCTest feature: Swift Testing, Apple’s newer framework, is for unit tests. Xcode 27 arrived on 14 September 2026, so check the release notes and your CI image before moving a suite onto it.
We have evaluated this tool rather than maintained a long-running suite on it, so this is a bounded view rather than a long-term verdict. The rest of this page is factual.
How it works
What is XCUITest and how does it work?
A runner app that drives yours
A UI test target builds a separate test runner. XCUIApplication is a proxy for the app under test: it launches, activates and terminates the target app and reports its state, while the test code runs in its own process. Apple described UI tests as running in a separate process and relying on accessibility from the start, and that model explains most of their behaviour. A test cannot call the app’s functions, read its view models or swap its dependencies. It can only see what the accessibility layer exposes and send synthesised taps, swipes and key presses. Anything the test needs to control inside the app goes through launch arguments and environment variables set before launch.
Queries resolve when they are used
app.buttons["login.submit"] does not find anything when the line runs; it builds an XCUIElementQuery that is resolved against the current accessibility hierarchy when you tap it, read a property or check exists. That makes element references cheap and always fresh, so stale references are rare. It also means ambiguity surfaces late: a query that matches several elements fails at the moment of interaction, which is why unique accessibility identifiers matter more than in most frameworks. Actions such as tap need a hittable point, so an element covered by another view or off screen fails even though it exists.
Waiting: idle checks plus explicit waits
Before synthesising each event, XCTest waits for the app to go idle, so simple sequences need no waits. It cannot know that a network response or an animation after that point will change the screen, so assertions about content that appears later need an explicit wait. waitForExistence(timeout:) returns false rather than failing when time runs out, so wrap it in XCTAssertTrue; waitForNonExistence(timeout:) covers spinners and dismissed sheets; and wait(for:toEqual:timeout:) waits for a property such as value or isEnabled to reach a given state. Fixed sleeps are never needed and always cost time.
XCUIAutomation, XCTest and Swift Testing
Apple now documents the UI automation types, XCUIApplication, XCUIElement, XCUIElementQuery, XCUIDevice and the rest, in a framework called XCUIAutomation, available from Xcode 16.3, while XCTest provides the test cases, assertions and runner. Both XCUIApplication and XCUIElement are main-actor classes, which is why Xcode’s templates mark UI test methods @MainActor. Swift Testing, introduced with Xcode 16, is Apple’s recommended framework for new unit tests, but Apple’s documentation is explicit that UI tests and performance tests continue to use XCTest. One test target can contain both frameworks, but a single test should not mix their APIs.
Strengths
What it does well
Recording that suggests queries
The record button in a UI test method launches the app and writes element queries and actions as you interact. For each element Xcode offers alternative queries, for example by label or by position, so you can pick the one least likely to break. Recording is a quick way to learn an unfamiliar hierarchy; the generated code is a draft to refactor, not a finished test.
System UI and other apps
A UI test can drive any app installed on the device or simulator through XCUIApplication(bundleIdentifier:), including Settings, and can respond to system alerts. resetAuthorizationStatus(for:) resets a protected resource such as location or photos, so permission prompts can be tested from a known state. That cross-app reach is something in-process Android frameworks lack.
Accessibility audits and VoiceOver checks
performAccessibilityAudit, available from iOS 17, scans the current screen for issues such as missing labels, low contrast and clipped text, and fails the test with details. Xcode 27 adds XCUIVoiceOverService, which drives VoiceOver from a UI test to verify focus order, spoken output and navigation. Both turn accessibility from a manual review into a repeatable check.
Evidence in result bundles
Screenshots, attachments and named activities are recorded into an .xcresult bundle, which xcodebuild writes on every run and Xcode opens with the full timeline of each test. Grouping steps with XCTContext activities makes long tests readable in that timeline, and attaching a screenshot at the point of failure saves a rerun.
Devices, remotes and configurations
XCUIDevice simulates hardware buttons, orientation and Siri interaction, and XCUIRemote drives tvOS apps. Test plans run the same tests under several configurations, such as different languages, regions, environment variables or sanitisers, so localisation and right-to-left layouts are covered without duplicating tests.
Parallel runs and repetition
Xcode can run test classes in parallel across clones of a simulator, and xcodebuild exposes the same with -parallel-testing-enabled. For flakiness work, -test-iterations with -run-tests-until-failure repeats a test until it breaks, and -retry-tests-on-failure gives failing tests another attempt, which is useful in CI as long as retried passes are still reviewed.
Pricing
XCUITest pricing and licensing
XCUITest costs nothing to use: XCTest and XCUIAutomation ship with Xcode, a free download that needs a Mac. It is proprietary Apple software rather than open source. A free Apple Account is enough to run tests on simulators and on your own devices through a personal team, with limits. The Apple Developer Program adds TestFlight, App Store distribution and Xcode Cloud, and the larger recurring cost is Mac hardware or macOS CI minutes.
- Licence: proprietary, included free with Xcode under Apple’s terms. The open-source swift-corelibs-xctest covers unit testing on other platforms and has no UI testing.
- Free Apple Account: simulators plus up to three registered devices through a personal team, with provisioning profiles that expire after seven days.
- Xcode Cloud: Apple Developer Program membership includes 25 compute hours a month; extra hours are monthly subscriptions, from 100 hours at US$49.99 a month at list price on 13 October 2026.
- Infrastructure: every UI test run needs macOS, either Macs you own or hosted macOS runners, which typically cost more per minute than Linux runners.
- Hidden cost: keeping accessibility identifiers, launch-argument hooks and test plans current as the app changes.
Checked against the vendor’s own pages on 13 October 2026. Pricing changes; confirm on the official site before buying.
Getting started
XCUITest tutorial: getting started
Setup, step by step
- Install Xcode 27 on a Mac running macOS Tahoe 26.6 or later, or stay on Xcode 26.x until your CI image has Xcode 27.
- Add a UI Testing Bundle target to the project, or choose a test system when creating a new project.
- Give the views you will test accessibility identifiers in the app code.
- Write a test method marked @MainActor that launches XCUIApplication and drives the screen.
- Run it from the diamond in the editor gutter, with Command-U, or with xcodebuild test.
Install
# Install Xcode from the Mac App Store, then point the command line tools at it
sudo xcode-select -s /Applications/Xcode.app
xcodebuild -version
# Run the UI tests on a simulator
xcodebuild test -scheme MyApp -destination 'platform=iOS Simulator,name=iPhone 17'A first example
// MyAppUITests/LoginUITests.swift
import XCTest
final class LoginUITests: XCTestCase {
override func setUpWithError() throws {
// Stop at the first failure: later steps depend on earlier ones
continueAfterFailure = false
}
@MainActor // XCUIApplication and XCUIElement are main-actor types
func testValidLoginShowsWelcome() throws {
let app = XCUIApplication()
// The app reads this flag to use a stubbed backend
app.launchArguments += ["-uitesting"]
app.launch()
let email = app.textFields["login.email"]
XCTAssertTrue(email.waitForExistence(timeout: 5))
email.tap()
email.typeText("[email protected]")
let password = app.secureTextFields["login.password"]
password.tap()
password.typeText("correct-horse")
app.buttons["login.submit"].tap()
// waitForExistence returns false on timeout, so assert on it
XCTAssertTrue(app.staticTexts["Welcome back"].waitForExistence(timeout: 10))
}
}What trips people up first
Elements that exist on screen but not to the test. A query by label text works until someone changes the copy or runs the app in another language, and a custom view with no accessibility information may not appear in the hierarchy at all. Pause in the debugger and print app.debugDescription to see what XCTest actually sees, then add accessibilityIdentifier values in UIKit or the .accessibilityIdentifier modifier in SwiftUI and query by those. The second early trap is typing: tap a text field before typeText, because typing needs keyboard focus.
In practice
Using XCUITest in practice
Identifiers and screen objects
Agree a naming scheme for accessibility identifiers, such as screen.element, and set them in the app: accessibilityIdentifier on UIKit views, the .accessibilityIdentifier modifier in SwiftUI. Identifiers are not spoken by VoiceOver, so they can be stable and technical without harming accessibility. In the test target, wrap each screen in a small type that holds its queries and exposes user-level actions, and return the next screen from actions that navigate. Tests then read as journeys, and an identifier change is fixed in one place. Mark these types @MainActor, because they hold XCUIElement values.
// In the app (SwiftUI)
TextField("Email", text: $email)
.accessibilityIdentifier("login.email")
// MyAppUITests/Screens/LoginScreen.swift
import XCTest
@MainActor
struct LoginScreen {
let app: XCUIApplication
var email: XCUIElement { app.textFields["login.email"] }
var password: XCUIElement { app.secureTextFields["login.password"] }
var submit: XCUIElement { app.buttons["login.submit"] }
@discardableResult
func signIn(_ user: String, _ secret: String) -> HomeScreen {
XCTAssertTrue(email.waitForExistence(timeout: 5))
email.tap()
email.typeText(user)
password.tap()
password.typeText(secret)
submit.tap()
return HomeScreen(app: app)
}
}Controlling app state through launch arguments
Because the test runs in another process, it cannot reset a database or swap a network client directly. The supported channel is launchArguments and launchEnvironment on XCUIApplication, read by the app at start-up through ProcessInfo. Use them to point the app at a stub server or local fixtures, start from a clean or seeded state, skip onboarding and switch off animations. Keep the test hooks behind a single check in the app so they cannot leak into release behaviour, and launch a fresh app in each test so no state carries over.
// In the app, at start-up
let args = ProcessInfo.processInfo.arguments
if args.contains("-uitesting") {
UIView.setAnimationsEnabled(false) // faster, steadier UI tests
AppEnvironment.current = .stubbed( // fixtures instead of the network
fixture: ProcessInfo.processInfo.environment["FIXTURE"] ?? "default"
)
}
// In a UI test
@MainActor
func testEmptyOrdersState() throws {
let app = XCUIApplication()
app.launchArguments += ["-uitesting"]
app.launchEnvironment["FIXTURE"] = "no-orders"
app.launch()
XCTAssertTrue(app.staticTexts["orders.empty"].waitForExistence(timeout: 5))
}Running in CI with xcodebuild
Split the job into build-for-testing and test-without-building, so the compile step is cached or reused and test retries do not rebuild. Name the destination exactly, including the simulator runtime, because hosted macOS images ship a fixed set of runtimes and devices. On GitHub’s macos-26 image, Xcode 26.6 is the default and Xcode 27 is offered as a preview image under a separate runner label, so pin the Xcode path. Write a result bundle and upload it when the job fails; it holds screenshots, logs and the timeline. Keep retries on for CI only, and review any test that needed one.
# .github/workflows/ios-ui-tests.yml
name: ios-ui-tests
on: [push, pull_request]
jobs:
xcuitest:
runs-on: macos-26
steps:
- uses: actions/checkout@v7
- name: Select Xcode
run: sudo xcode-select -s /Applications/Xcode_26.6.app
- name: Build for testing
run: >
xcodebuild build-for-testing
-scheme MyApp
-destination 'platform=iOS Simulator,name=iPhone 17,OS=26.5'
-derivedDataPath build
- name: Run UI tests
run: >
xcodebuild test-without-building
-scheme MyApp
-destination 'platform=iOS Simulator,name=iPhone 17,OS=26.5'
-derivedDataPath build
-only-testing MyAppUITests
-parallel-testing-enabled YES
-retry-tests-on-failure -test-iterations 2
-resultBundlePath TestResults.xcresult
- uses: actions/upload-artifact@v7
if: failure()
with:
name: ui-test-results
path: TestResults.xcresultPermission prompts and other apps
System alerts belong to the system, not your app, and a prompt that appears mid-test blocks the next tap. There are three ways to keep them under control. Put the app in a known authorisation state before launch with resetAuthorizationStatus(for:), so the prompt appears exactly when the test expects it, then tap the alert’s button through the system UI. Register addUIInterruptionMonitor handlers for prompts that may or may not appear; XCTest calls them when an unexpected alert blocks an interaction. And for journeys that leave the app, such as changing a setting, create an XCUIApplication with that app’s bundle identifier, act there, then activate your app again.
Test plans and diagnosing failures
Use test plans to separate a fast smoke set from the full regression run and to repeat tests across configurations, for example English and Arabic, or a stubbed and a staging environment through environment variables. xcodebuild selects a plan with -testPlan and lists them with -showTestPlans. Xcode 27 adds a test plan setting for how a crash of the target app during UI testing is treated, from off to fatal failure, which is worth setting deliberately rather than leaving at the default failure. When a test fails, open the .xcresult bundle first: the timeline shows each query, the screenshot at failure and the hierarchy XCTest saw. For intermittent failures, rerun the single test with -test-iterations and -run-tests-until-failure before changing any waits.
Pitfalls
Common pitfalls
Using sleep to wait for content
Why it happens: XCTest waits for the app to go idle before events, but not for data that arrives later. A sleep hides the race on a fast Mac and fails on a loaded CI runner.
The fix: Wait on the condition: waitForExistence(timeout:) wrapped in XCTAssertTrue, waitForNonExistence(timeout:) for spinners, or wait(for:toEqual:timeout:) for a property value.
Querying by visible text
Why it happens: Labels change with copy edits, dynamic content and localisation, so tests break for reasons unrelated to behaviour, and test plans in other languages fail entirely.
The fix: Query by accessibility identifiers set in the app, and reserve label queries for asserting the text the user should see.
Concurrency errors after moving to Swift 6
Why it happens: XCUIApplication and XCUIElement are main-actor classes. Helpers or stored properties that touch them from non-isolated code fail to compile under strict concurrency checking.
The fix: Mark UI test methods, screen objects and helpers @MainActor, as Xcode’s templates do, and create the app inside the test method rather than in a non-isolated property.
Expecting Swift Testing to run UI tests
Why it happens: Swift Testing is Apple’s recommended framework for new unit tests, so teams assume it replaces XCTest everywhere.
The fix: Keep UI tests in XCTestCase subclasses, as Apple’s documentation directs, and use Swift Testing for unit tests in the same project.
Tests that only pass in a particular order
Why it happens: A previous test left the app logged in, a permission granted or data created, and the next test quietly relied on it.
The fix: Launch a fresh app per test with launch arguments that reset or seed state, and reset protected resources with resetAuthorizationStatus(for:) where permissions matter.
CI fails to find the destination
Why it happens: Hosted macOS images ship specific Xcode versions, simulator runtimes and device names, and a destination copied from a laptop may not exist there.
The fix: Pin the Xcode path, check the runner image’s list of installed simulators, and name the device and OS version explicitly in -destination.
Compared
How XCUITest compares
| Tool | Test languages | Platforms | Process model | Host requirement | Licence |
|---|---|---|---|---|---|
| XCUITest | Swift, Objective-C | iOS, iPadOS, macOS, tvOS, watchOS | Separate runner through accessibility | Mac with Xcode | Proprietary, free with Xcode |
| Appium | Java, Python, JS, C#, Ruby | iOS, Android, tvOS, desktop | WebDriver server driving WebDriverAgent on iOS | Mac with Xcode for iOS | Apache 2.0 |
| Maestro | YAML, optional JavaScript | iOS simulators, Android, web | Black box via accessibility layer | Mac with Xcode for iOS | Apache 2.0 |
| Detox | JavaScript, TypeScript | React Native on iOS, Android | Grey box, instrumented app build | Mac with Xcode for iOS | MIT |
FAQ
Frequently asked questions
What is XCUITest?
XCUITest is the common name for Apple’s UI testing support: tests written in Swift or Objective-C with XCTest, driving the app through the XCUIAutomation framework from a separate test runner. It ships free with Xcode and covers iOS, iPadOS, macOS, tvOS and watchOS apps. Xcode 27, current since 14 September 2026, is the version reviewed here.
Is XCUITest free?
Yes. XCTest and XCUIAutomation are included with Xcode, which is a free download for macOS, and a free Apple Account can run UI tests on simulators and on a few personal devices. It is proprietary rather than open source. Costs come from Mac hardware or macOS CI minutes, and from the Apple Developer Program if you need TestFlight, App Store distribution or Xcode Cloud.
Should I use XCUITest or Appium?
Use XCUITest when iOS developers own the UI tests and write Swift: there is no extra server or driver, Xcode supports it directly and failures are easier to trace. Use Appium when one team must cover iOS and Android in one language, or the testers are not Swift developers. On iOS, Appium drives XCUITest underneath through WebDriverAgent, so a native XCUITest suite skips those extra layers.
Can I write UI tests with Swift Testing?
No. Swift Testing, introduced with Xcode 16, is Apple’s recommended framework for new unit tests, but Apple’s testing documentation says to continue using XCTest for user interface tests and performance tests. A test target can hold both kinds, so unit tests can move to Swift Testing while UI tests stay in XCTestCase subclasses.
What changed for UI tests in Xcode 27?
Xcode 27, made available on 14 September 2026, adds XCUIVoiceOverService for checking VoiceOver behaviour from UI tests, a test plan setting for how target app crashes during UI testing are treated, a launch test template that runs across orientations, localisations and appearances, and warnings when XCTest and Swift Testing assertions are mixed. It requires macOS Tahoe 26.6 or later.
Can XCUITest run on real iPhones?
Yes. Choose a connected device as the destination in Xcode, or pass its identifier to xcodebuild with platform=iOS. The app and test runner must be signed: a free Apple Account works for your own devices through a personal team, with profiles that expire after seven days, while teams usually use Apple Developer Program signing or a device cloud for broader coverage.
How do I handle the location or camera permission prompt?
Decide whether the test is about the prompt. If it is, reset the resource with resetAuthorizationStatus(for:) before launch, trigger the prompt and tap its button through the system UI. If it is not, register an addUIInterruptionMonitor handler that dismisses the alert when it blocks an interaction, or start the app in a state that does not ask, so the test stays focused.
Is XCUITest worth learning in 2026?
Yes for anyone testing Apple platforms. It is the layer every other iOS UI tool, including Appium, ultimately drives, so knowing how its queries, waits and accessibility hierarchy behave makes failures in any iOS tool easier to understand. Pair it with Swift Testing for unit tests, since that is where Apple is steering new unit test work.
Alternatives
XCUITest alternatives worth weighing
Resources
Where to go next
Keep going
Prepare, or get it done
Picking the wrong tool costs more than picking late.
QAble advises on toolchain decisions and then implements them, with engineers who have run these tools on real delivery. Start with a free QA audit.