From qa-desktop
Authors and runs XCTest UI + unit tests for macOS desktop apps - the Apple-first-party test framework that ships with Xcode. Covers the `XCTestCase` subclass + `test*` method-naming convention, `XCUIApplication` / `XCUIElement` / `XCUIElementQuery` for UI tests, accessibility-identifier-based locators (the stable replacement for label-based queries), `XCTAssert*` macros, `measureBlock:` for performance regressions, and `xcodebuild test` for CI execution. Use when the macOS app is built with Xcode and the test target is in-tree alongside the app - for cross-OS sharing see Appium Mac2 driver as a separate path.
How this skill is triggered — by the user, by Claude, or both
Slash command
/qa-desktop:xctest-mac-desktopThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
XCTest is the **first-party test framework** bundled with Xcode. Per
XCTest is the first-party test framework bundled with Xcode. Per Apple's Testing with Xcode - UI Testing chapter:
"UI testing rests upon two core technologies: the XCTest framework and Accessibility."
This is the same framework used for unit tests and performance tests - UI testing is layered on top via three classes (appleuit):
"UI Testing in Xcode rests on two core technologies: the XCTest framework and Accessibility … XCUIApplication … XCUIElement … XCUIElementQuery."
This skill wraps XCTest for macOS desktop apps. For iOS / iPadOS the same APIs apply with different launch + simulator semantics - that path is intentionally out of scope; this plugin covers desktop only.
Strategic frame: see
desktop-test-strategy-reference
for how macOS sits in the three-OS landscape (UIA on Windows, XCTest
on macOS, AT-SPI on Linux). The locator strategy across all three
backends converges on accessibility identifiers.
xcodebuild test).XCTestCase base).In Xcode: File → New → Target → UI Testing Bundle. The
generated test class inherits from XCTestCase and ships with a
boilerplate setUp that launches the app
(appleuit):
import XCTest
final class CheckoutUITests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = false // recommended by Apple — UI steps depend on prior steps
XCUIApplication().launch()
}
func testCheckoutHappyPath() throws {
// Test body — see Step 3
}
}
Per appleuit:
"Set continueAfterFailure to NO ensures tests stop on first failure (recommended since UI test steps are dependent)."
Per applewt, a test method must:
test"void"Lifecycle order (applewt):
+ (void)setUp) - once before all tests.setUp → test method → tearDown.+ (void)tearDown) - once after all tests.The portable lesson per
desktop-test-strategy-reference:
prefer accessibilityIdentifier over visible labels. Set the
identifier in app code:
// In app code (SwiftUI)
Button("Sign In") { … }
.accessibilityIdentifier("signInButton")
// Or in AppKit
signInButton.setAccessibilityIdentifier("signInButton")
Then in tests:
func testCheckoutHappyPath() throws {
let app = XCUIApplication()
app.launch()
// Query → Interact → Assert (the canonical XCUI pattern per [appleuit])
app.textFields["emailField"].tap()
app.textFields["emailField"].typeText("[email protected]")
app.secureTextFields["passwordField"].tap()
app.secureTextFields["passwordField"].typeText("s3cret")
app.buttons["signInButton"].tap()
// Wait + assert
let welcomeHeading = app.staticTexts["welcomeHeading"]
XCTAssertTrue(welcomeHeading.waitForExistence(timeout: 5))
XCTAssertEqual(welcomeHeading.label, "Welcome, [email protected]")
}
Per appleuit, the canonical pattern is:
"Use an XCUIElementQuery to find an XCUIElement. Synthesize an event and send it to the XCUIElement. Use an assertion to compare the state of the XCUIElement against an expected reference state."
waitForExistence(timeout:) is the documented predicate-polling
primitive used in place of fixed sleeps (stable identifier in Apple's
XCUIElement reference; cited inline by name).
Per applewt, XCTAssert macros fall into five categories:
| Category | Macros |
|---|---|
| Equality | XCTAssertEqual, XCTAssertEqualObjects, XCTAssertNotEqual, XCTAssertGreaterThan, XCTAssertEqualWithAccuracy |
| Boolean | XCTAssertTrue, XCTAssertFalse |
| Nil | XCTAssertNil, XCTAssertNotNil |
| Exception | XCTAssertThrows, XCTAssertThrowsSpecific, XCTAssertNoThrow |
| Unconditional fail | XCTFail |
All accept an optional format string for the failure message (applewt).
Per applewt, performance tests "run a code block 10 times, collecting average execution time and standard deviation":
func testAdditionPerformance() throws {
self.measure {
var sum = 0
for i in 0..<100_000 { sum += i }
XCTAssertEqual(sum, 4_999_950_000)
}
}
Per applewt: "Performance tests report failure on first run until a baseline is set. Baselines are stored per-device- configuration." Practical implication: the first CI run on a new Mac architecture (Intel → Apple Silicon migration) fails until the baseline is committed.
Per appleuit, Xcode's "UI Recording" workflow generates test code from interactive use:
XCTAssert assertions to validate behaviour.Treat recordings as a starting point - the generated locator
chain tends to rely on label paths rather than
accessibilityIdentifier. Refactor to identifier-based queries (per
the desktop-test-strategy-reference
locator table) before checking in.
From the command line:
# Run the full test bundle
xcodebuild test \
-project MyApp.xcodeproj \
-scheme MyApp \
-destination 'platform=macOS' \
-resultBundlePath build/result.xcresult
# Run a single test class
xcodebuild test \
-project MyApp.xcodeproj \
-scheme MyApp \
-destination 'platform=macOS' \
-only-testing:MyAppUITests/CheckoutUITests
# Run a single test method
xcodebuild test \
-project MyApp.xcodeproj \
-scheme MyApp \
-destination 'platform=macOS' \
-only-testing:MyAppUITests/CheckoutUITests/testCheckoutHappyPath
-destination 'platform=macOS' targets the host Mac. -resultBundlePath
writes a .xcresult bundle that contains attachments (screenshots
on failure, performance metrics, logs) - the canonical artefact for
post-mortem.
The .xcresult bundle is queryable via xcrun xcresulttool:
# JSON summary of the result bundle
xcrun xcresulttool get --path build/result.xcresult --format json
# Extract a specific failure's screenshot attachment
xcrun xcresulttool get --path build/result.xcresult \
--id <attachment-id> --output failure.png
For CI dashboards that expect JUnit XML, the open-source xcresultparser
project converts .xcresult → JUnit XML; pair downstream with
junit-xml-analysis.
# .github/workflows/macos-xctest.yml
jobs:
test:
runs-on: macos-14 # Apple Silicon
steps:
- uses: actions/checkout@v5
- uses: maxim-lobanov/setup-xcode@v1
with: { xcode-version: '15.4' }
- name: Build + test
run: |
xcodebuild test \
-project MyApp.xcodeproj \
-scheme MyApp \
-destination 'platform=macOS' \
-resultBundlePath build/result.xcresult \
-enableCodeCoverage YES
- uses: actions/upload-artifact@v4
if: always()
with:
name: xcresult
path: build/result.xcresult
Hosted macOS runners on GitHub-hosted are interactive sessions - XCUIApplication launches work without extra display setup. Self- hosted Mac headless setups need an attached console or VNC session; XCTest UI cannot run under launchd alone.
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Querying by visible label (app.buttons["Sign In"]) | Localisation collapses the locator (Spanish, Japanese builds break) | accessibilityIdentifier per Step 3 (per desktop-test-strategy-reference) |
XCUIApplication().launch() inside every test method | Per-method app launch is slow + redundant | Launch in setUp (appleuit) |
Thread.sleep(2.0) between actions | Flaky on slow CI, slow on fast | waitForExistence(timeout:) predicate polling (Step 3) |
continueAfterFailure = true for UI tests | First failure cascades into confusing follow-on failures | continueAfterFailure = false per appleuit |
| Mixing UI + unit + performance in one test method | Result attribution is opaque | One method per behaviour; share setup via setUp (applewt) |
| Performance baseline committed from a developer Mac | Baselines are device-specific; CI runner is a different device | Commit baselines from the CI runner that will gate the PR (applewt) |
| Recording-and-keep workflow without identifier refactor | Generated label-path locators are brittle | Refactor recordings to accessibilityIdentifier (Step 6) |
XCUIApplication() without .launch() | The query tree is empty; element lookups time out | Always launch() before any query (appleuit) |
desktop-test-strategy-reference
matrix).developer.apple.com/documentation/xctest SPA shell returns
without body content via automated fetch; this skill cites
Apple's stable library/archive testing-with-xcode chapter
(appleuit, applewt) for prose and treats
per-API surface (XCUIApplication, XCUIElement, XCUIElementQuery,
waitForExistence(timeout:)) as stable identifiers in Apple's
XCUIElement reference. Document this in the PR description if the
reviewer asks for a click-through link.XCUICoordinate interactions; complex multi-app
flows often need Apple Mac2 driver via Appium for parity with
Windows / Linux test sources.desktop-test-strategy-reference.XCTestObservationCenter
for in-process observation rather than out-of-process file diffs.desktop-test-strategy-reference.winappdriver (Windows UIA),
at-spi-linux (Linux AT-SPI),
qt-test-framework (Qt in-process).junit-xml-analysis.npx claudepluginhub testland/qa --plugin qa-desktopGuides creation, editing, and verification of skills for AI coding agents using test-driven development with subagent scenarios. Use when authoring or debugging skills.