XCUITest
Set up XCUITest for iOS UI testing and integrate test results with TestKase.
Overview
XCUITest is Apple's native UI testing framework for iOS, macOS, tvOS, and watchOS applications. Tests are written in Swift or Objective-C and run within Xcode, interacting with the app through accessibility elements. XCUITest is tightly integrated with the Xcode toolchain and provides reliable, first-party UI automation.
To integrate XCUITest results with TestKase, first convert the .xcresult bundle to JUnit XML format,
then report with --format junit.
Prerequisites
- macOS (required for Xcode)
- Xcode 15+ with command-line tools installed
- An iOS Simulator or physical device enrolled in your development team
- A result converter:
xcresult-to-junitor theswift-junitpackage
Installation
XCUITest is built into Xcode — no additional framework installation is needed. To add a UI Testing target to your project:
- In Xcode, go to File > New > Target
- Select UI Testing Bundle
- Name it (e.g.,
MyAppUITests) and click Finish
Install a converter tool to transform .xcresult bundles into JUnit XML:
brew install nicklama/tap/xcresult-to-junitAlternatively, you can use the xcresulttool built into Xcode to extract test data and convert it
with a custom script.
Project Setup
UI Test Target Structure
After creating the UI Testing target, Xcode generates the following structure:
MyApp/
MyApp/ # Main app source
MyAppTests/ # Unit tests
MyAppUITests/ # UI tests (XCUITest)
MyAppUITests.swift
MyAppUITestsLaunchTests.swiftScheme Configuration
Ensure your scheme includes the UI test target:
- Click the scheme selector in the Xcode toolbar
- Select Edit Scheme
- Under the Test action, verify that
MyAppUITestsis listed and enabled
If you are running tests from the command line, you can list available schemes with
xcodebuild -list -project MyApp.xcodeproj or xcodebuild -list -workspace MyApp.xcworkspace.
Writing Tests
Create a test class in the UI test target:
// MyAppUITests/LoginUITests.swift
import XCTest
final class LoginUITests: XCTestCase {
let app = XCUIApplication()
override func setUpWithError() throws {
continueAfterFailure = false
app.launch()
}
func testValidLogin() throws {
let emailField = app.textFields["emailInput"]
emailField.tap()
emailField.typeText("user@example.com")
let passwordField = app.secureTextFields["passwordInput"]
passwordField.tap()
passwordField.typeText("password123")
app.buttons["loginButton"].tap()
let dashboard = app.staticTexts["dashboardTitle"]
XCTAssertTrue(dashboard.waitForExistence(timeout: 5))
}
func testInvalidPassword() throws {
let emailField = app.textFields["emailInput"]
emailField.tap()
emailField.typeText("user@example.com")
let passwordField = app.secureTextFields["passwordInput"]
passwordField.tap()
passwordField.typeText("wrong")
app.buttons["loginButton"].tap()
let errorMessage = app.staticTexts["errorMessage"]
XCTAssertTrue(errorMessage.waitForExistence(timeout: 5))
XCTAssertEqual(errorMessage.label, "Invalid credentials")
}
func testForgotPasswordNavigation() throws {
app.buttons["forgotPasswordLink"].tap()
let resetTitle = app.staticTexts["resetPasswordTitle"]
XCTAssertTrue(resetTitle.waitForExistence(timeout: 5))
}
}Link 5-digit Automation IDs from TestKase to each test. Since Swift method names cannot contain
brackets, embed the ID in the method name using underscores (e.g., func test_48271_ValidLogin()) and
configure the reporter with --automation-id-format "_(\\d{5})_".
For the tests above, generate IDs in TestKase and link them:
48271→ linked to the "valid login" test case in TestKase48272→ linked to the "invalid password" test case in TestKase48273→ linked to the "forgot password" test case in TestKase
Generate Automation IDs in TestKase first. For XCUITest where method names cannot contain brackets,
use the --automation-id-format CLI flag to customize the extraction regex.
Running Tests
Run UI tests from the command line using xcodebuild:
xcodebuild test \
-scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 15' \
-resultBundlePath test-results/results.xcresultAfter the test run completes, convert the .xcresult bundle to JUnit XML:
xcresult-to-junit test-results/results.xcresult > test-results/junit.xmlFor workspaces (.xcworkspace), use the -workspace flag instead of the default project:
xcodebuild test -workspace MyApp.xcworkspace -scheme MyApp ...
TestKase Integration
After converting the test results to JUnit XML, report results to TestKase:
npx @testkase/reporter report \
--token $TESTKASE_PAT \
--project-id PRJ-1 \
--org-id 1173 \
--cycle-id TCYCLE-5 \
--format junit \
--results-file test-results/junit.xml--cycle-id is optional. If not provided, results are reported to TCYCLE-1 — the master test cycle for the project.
Automation ID Mapping
The reporter extracts Automation IDs based on the configured pattern. For XCUITest, embed the 5-digit ID
in the method name and use a custom --automation-id-format:
| Approach | Test Method | CLI Flag | Extracted ID |
|---|---|---|---|
| Underscore pattern | func test_48271_ValidLogin() | --automation-id-format "_(\\d{5})_" | 48271 |
Generate the 5-digit ID in TestKase first, then embed it in your test method name.
Complete Example
1. Test File
// MyAppUITests/LoginUITests.swift
import XCTest
final class LoginUITests: XCTestCase {
let app = XCUIApplication()
override func setUpWithError() throws {
continueAfterFailure = false
app.launch()
}
func testValidLogin() throws {
app.textFields["emailInput"].tap()
app.textFields["emailInput"].typeText("user@example.com")
app.secureTextFields["passwordInput"].tap()
app.secureTextFields["passwordInput"].typeText("password123")
app.buttons["loginButton"].tap()
XCTAssertTrue(app.staticTexts["dashboardTitle"].waitForExistence(timeout: 5))
}
func testInvalidPassword() throws {
app.textFields["emailInput"].tap()
app.textFields["emailInput"].typeText("user@example.com")
app.secureTextFields["passwordInput"].tap()
app.secureTextFields["passwordInput"].typeText("wrong")
app.buttons["loginButton"].tap()
let errorMessage = app.staticTexts["errorMessage"]
XCTAssertTrue(errorMessage.waitForExistence(timeout: 5))
XCTAssertEqual(errorMessage.label, "Invalid credentials")
}
}2. Run Tests and Convert Results
# Run UI tests and generate xcresult bundle
xcodebuild test \
-scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 15' \
-resultBundlePath test-results/results.xcresult
# Convert xcresult to JUnit XML
xcresult-to-junit test-results/results.xcresult > test-results/junit.xml3. Report Results to TestKase
npx @testkase/reporter report \
--token $TESTKASE_PAT \
--project-id PRJ-1 \
--org-id 1173 \
--cycle-id TCYCLE-5 \
--format junit \
--results-file test-results/junit.xmlTroubleshooting
xcresult conversion fails
Ensure the xcresult-to-junit tool version is compatible with your Xcode version. The .xcresult format
can change between Xcode releases. Update the converter:
brew upgrade nicklama/tap/xcresult-to-junitIf the tool does not support your Xcode version yet, you can use xcresulttool (bundled with Xcode) to
extract raw JSON and convert it manually:
xcrun xcresulttool get --format json --path test-results/results.xcresultSimulator not booted
If xcodebuild fails because the simulator is not available, boot it manually before running tests:
xcrun simctl boot "iPhone 15"List all available simulators and their states:
xcrun simctl list devices availableIf the simulator you need is not installed, create one:
xcrun simctl create "iPhone 15" "com.apple.CoreSimulator.SimDeviceType.iPhone-15" "com.apple.CoreSimulator.SimRuntime.iOS-17-2"Test target not found
If xcodebuild reports that no test target is found, verify that your scheme includes the UI test target:
- Open the scheme editor: Product > Scheme > Edit Scheme
- Select the Test action in the left sidebar
- Click + and add
MyAppUITestsif it is not listed
From the command line, verify the scheme configuration:
xcodebuild -list -project MyApp.xcodeprojEnsure the scheme name you pass to -scheme exactly matches one of the listed schemes.