xctest-mac-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.
Install with skills.sh (any agent)
npx skills add testland/qa --skill xctest-mac-desktopxctest-mac-desktop
Overview
XCTest is the first-party test framework bundled with Xcode, used for unit, performance, and UI tests. Per Apple's Testing with Xcode - UI Testing chapter (opens in new window), "UI Testing in Xcode rests on two core technologies: the XCTest framework and Accessibility," layered via three classes - XCUIApplication, XCUIElement, and XCUIElementQuery (appleuit (opens in new window)).
This skill wraps XCTest for macOS desktop apps. iOS / iPadOS use the same APIs with different launch + simulator semantics and are out of scope here.
Strategic frame: desktop-test-strategy-reference places macOS in the three-OS landscape (UIA on Windows, XCTest on macOS, AT-SPI on Linux); the locator strategy converges on accessibility identifiers across all three.
When to use
Step 1 - Create a UI test target
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 (opens in new window)):
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 (opens in new window):
"Set continueAfterFailure to NO ensures tests stop on first failure (recommended since UI test steps are dependent)."
Step 2 - Test-method naming + lifecycle
Per applewt (opens in new window), a test method must "begin with the prefix test", take no parameters, and return void. Lifecycle order:
Step 3 - Author UI tests with accessibility identifiers
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("user@example.com")
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, user@example.com")
}Per appleuit (opens in new window), 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).
Step 4 - Assertion macros
Per applewt (opens in new window), 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 (opens in new window)).
Step 5 - Performance tests with measureBlock:
Per applewt (opens in new window), 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 (opens in new window): "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.
Step 6 - Recording UI tests
Per appleuit (opens in new window), Xcode's "UI Recording" workflow generates test code from interactive use:
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.
Step 7 - Run
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.
Step 8 - Parsing results
Query the .xcresult bundle with xcrun xcresulttool (JSON summary, attachment extraction), then convert to JUnit XML via the open-source xcresultparser for junit-xml-analysis. Commands: references/ci-and-results.md.
Step 9 - CI integration
Hosted macOS runners are interactive, so xcodebuild test UI launches need no extra display setup; self-hosted headless Macs need an attached console or VNC session (XCTest UI cannot run under launchd alone). Full workflow: references/ci-and-results.md.
Anti-patterns
| 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 (opens in new window)) |
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 (opens in new window) |
| Mixing UI + unit + performance in one test method | Result attribution is opaque | One method per behaviour; share setup via setUp (applewt (opens in new window)) |
| 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 (opens in new window)) |
| 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 (opens in new window)) |
Limitations
References
xctest-mac-desktop - results parsing and CI
View source (opens in new window)xctest-mac-desktop - results parsing and CI
Results parsing and the CI workflow, kept out of the SKILL spine. See SKILL.md (opens in new window) for authoring and running XCTest UI tests.
Parsing results
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.pngFor CI dashboards that expect JUnit XML, the open-source xcresultparser project converts .xcresult -> JUnit XML; pair downstream with junit-xml-analysis.
CI integration
Hosted macOS runners on GitHub-hosted are interactive sessions, so XCUIApplication launches work without extra display setup. Self-hosted headless Mac setups need an attached console or VNC session; XCTest UI cannot run under launchd alone.
# .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.xcresultRelated skills
appium-windows-driver
Authors and runs Appium 2.x tests against the Windows driver, the actively-maintained Node.js proxy in front of Microsoft's WinAppDriver: `appium driver install windows`, capabilities (`platformName: windows`, `appium:automationName: windows`, `appium:app`, `appium:appTopLevelWindow`, `appium:appArguments`), Windows gestures (`windows: scroll` / `clickAndDrag` / `keys`), PowerShell prerun/postrun hooks, and CI. Use when the stack already uses Appium for iOS / Android / Mac2 and Windows fits the existing client + capability model; to drive WinAppDriver directly from a Selenium-style client use winappdriver, and for a C#-only FlaUI client use flaui-tests.
at-spi-linux
Authors Linux desktop UI tests via AT-SPI - the DBus-based Assistive Technology Service Provider Interface implemented by `at-spi2-core` (registry daemon + `libatspi` C library + ATK GTK bridge). Covers enabling toolkit accessibility (`gsettings set org.gnome.desktop.interface toolkit-accessibility true`), driving GTK + Qt apps through Python `dogtail` (object-oriented + procedural APIs), inspecting the tree with Accerciser, scripting via `pyatspi`, and CI integration on headless Linux runners with `Xvfb` + `dbus-launch`. Use for Linux-side desktop tests of GTK applications, Qt apps with QAccessible enabled, and Electron apps on Linux.
desktop-test-strategy-reference
Pure-reference catalog of desktop GUI test strategies across Windows, macOS, and Linux. Defines the three accessibility-tree backends (Microsoft UI Automation on Windows, Apple Accessibility / XCTest on macOS, AT-SPI on Linux), the wrapper-tools that drive each backend (WinAppDriver, Appium-Windows, XCUIApplication, AT-SPI clients), the cross-toolkit Electron + Qt paths, and a per-OS decision matrix with accessibility-first locator strategy. Deep operational detail (per-OS asynchronous-wait hierarchies, parallel-test policy, foreground-lock / UAC / TCC / AT-SPI elevation hazards, and the high-DPI / per-monitor test matrix) lives in references/. Use when choosing how to test or automate a desktop GUI application (desktop app testing, GUI automation, automate desktop UI) on Windows, macOS, or Linux - the strategic reference before picking a desktop test stack, ahead of the per-tool implementation skills.
electron-playwright
Authors Playwright `_electron` tests for packaged Electron desktop apps - launches the app via `electron.launch({ args })`, returns an `ElectronApplication` handle, drives renderer windows as Playwright `Page` objects, and probes the main process via `electronApp.evaluate(({ app, BrowserWindow }) => …)`. Distinct from ordinary browser page automation: this wraps the `_electron` API for launching packaged Electron apps and probing main + renderer processes. Use for end-to-end tests of Electron apps where main-process state, IPC, and renderer DOM must all be asserted from one suite.
electron-spectron
Legacy reference for Spectron - Electron's original ChromeDriver-based testing framework, officially deprecated 2022-02-01 at v19.0.0. Documents what Spectron was, the architectural reason it became unmaintainable, the migration path to Playwright `_electron`, and the residual support contract for projects still on Spectron. Use only when auditing a legacy suite or planning a migration off Spectron - for new work use Playwright's `_electron` API.
flaui-tests
Authors and runs FlaUI-based Windows UI tests - the .NET-native wrapper around Microsoft UI Automation (UIA2 + UIA3). Covers the `FlaUI.Core` / `FlaUI.UIA2` / `FlaUI.UIA3` NuGet packages, `Application.Launch` / `Application.Attach` lifecycles, `ConditionFactory` + `FindFirstDescendant` locator patterns, `Retry` waits, and xUnit / NUnit / MSTest harness integration. Use when the test stack is C# / .NET-first and the team wants idiomatic in-process UIA calls rather than the HTTP/JSON wire protocol of `winappdriver` or the Appium proxy layer of `appium-windows-driver`.
qt-test-framework
Authors and runs Qt Test - the first-party C++ unit + GUI test framework that ships with Qt 6 (via the `QtTest` module header). Covers the `QTEST_MAIN` / `QTEST_APPLESS_MAIN` / `QTEST_GUILESS_MAIN` entry-point macros, the `QObject` private-slot test pattern, `QVERIFY` / `QCOMPARE` / `QFETCH` assertions, GUI event simulation (`QTest::mouseClick`, `QTest::keyClick`, `QTest::touchEvent`), `QSignalSpy` for signal introspection, `QBENCHMARK` for performance regression, and the `-o file,junitxml` CI output. Use for in-process testing of Qt widgets, QObject signal/slot chains, and Qt Quick / QML application logic; for out-of-process Qt-app driving, use an OS-native accessibility driver instead.
winappdriver
Authors and runs Windows UI tests against WinAppDriver, Microsoft's W3C-WebDriver service for UWP / WPF / WinForms / Win32 apps: installing + launching `WinAppDriver.exe` on `127.0.0.1:4723`, declaring `app` / `platformName` / `appArguments` / `appTopLevelWindow` capabilities, finding elements by `AccessibilityId` / `Name` / `ClassName`, and Windows-runner CI. Use when driving a native Windows app from a Selenium-style client (C#, Java, Python, Ruby, JS); for the actively-maintained Appium 2.x wrapper over the same server use appium-windows-driver, for a C#-only FlaUI client use flaui-tests, and to choose among Windows desktop drivers first use desktop-test-strategy-reference.