Skip to content
intermediate Phase 10 · Testing & Debugging

XCUITest & UI Testing

Write UI tests with XCUITest: element queries, gestures, navigation, and screenshot testing.

50m
3 problems
Topic Progress 0%

XCUIApplication & Element Queries

What is XCUITest?

XCUITest is Apple UI testing framework that launches your app in a simulator or device and interacts with it like a real user. Unlike unit tests that test individual components, UI tests verify entire user journeys from tap to screen change.

Setting Up a UI Test Target

Add a UI Testing Bundle to your project via File, New, Target. The default test file looks like:

import XCTest

class MyAppUITests: XCTestCase {
    let app = XCUIApplication()
    
    override func setUpWithError() throws {
        continueAfterFailure = false
        app.launch()
    }
    
    func testLoginFlow() throws {
        let emailField = app.textFields["emailTextField"]
        emailField.tap()
        emailField.typeText("user@example.com")
        
        let passwordField = app.secureTextFields["passwordTextField"]
        passwordField.tap()
        passwordField.typeText("password123")
        
        app.buttons["loginButton"].tap()
        
        XCTAssertTrue(app.staticTexts["Welcome"].waitForExistence(timeout: 5))
    }
}

XCUIApplication

XCUIApplication represents your app as a whole. Key methods include app.launch() to start the app, app.launchEnvironment to set environment variables, and app.launchArguments for command-line arguments.

Element Queries

Find elements using accessibility identifiers, labels, or type:

let button = app.buttons["submitButton"]
let label = app.staticTexts["Welcome back"]
let textField = app.textFields.firstMatch
let cell = app.tables.cells.containing(.staticText, identifier: "Item 1").element
let item = app.collectionViews.cells.element(boundBy: 0)

Accessibility Identifiers

Set accessibility identifiers in SwiftUI or UIKit to make UI tests reliable:

// SwiftUI
Button("Submit") { }
    .accessibilityIdentifier("submitButton")

// UIKit
submitButton.accessibilityIdentifier = "submitButton"

Always use accessibility identifiers rather than relying on button text which may change with localization.

Element Interaction & Waiting

Interacting with Elements

XCUITest provides methods to simulate real user interactions:

downloadButton.tap()
searchField.tap()
searchField.typeText("SwiftUI tutorial")
app.swipeLeft()
app.swipeUp()
cell.press(forDuration: 2.0)
app.orientation = .landscapeLeft
app.pinch(withScale: 2.0, velocity: 1.0)

Wait Strategies

UI tests must wait for animations, network calls, and screen transitions:

// Wait for element existence
let alert = app.alerts["Error"]
XCTAssertTrue(alert.waitForExistence(timeout: 10))

// Wait for element to disappear
let spinner = app.activityIndicators["loading"]
XCTAssertFalse(spinner.waitForExistence(timeout: 5))

// Wait for hittable (ready for interaction)
let payButton = app.buttons["Pay"]
let isHittable = payButton.waitForExistence(timeout: 5) && payButton.isHittable
XCTAssertTrue(isHittable)

Handling Alerts and Sheets

func testDeleteItem_showsConfirmation() {
    app.tables.cells.element(boundBy: 0).swipeLeft()
    app.buttons["Delete"].tap()
    
    let alert = app.alerts["Confirm Delete"]
    XCTAssertTrue(alert.waitForExistence(timeout: 2))
    
    alert.buttons["Cancel"].tap()
    XCTAssertFalse(alert.waitForExistence(timeout: 1))
}

Page Object Pattern

Organize UI tests using the Page Object pattern to reduce duplication:

class LoginPage {
    let app: XCUIApplication
    let emailField: XCUIElement
    let passwordField: XCUIElement
    let loginButton: XCUIElement
    
    init(app: XCUIApplication) {
        self.app = app
        self.emailField = app.textFields["emailField"]
        self.passwordField = app.secureTextFields["passwordField"]
        self.loginButton = app.buttons["loginButton"]
    }
    
    @discardableResult
    func login(email: String, password: String) -> HomePage {
        emailField.tap()
        emailField.typeText(email)
        passwordField.tap()
        passwordField.typeText(password)
        loginButton.tap()
        return HomePage(app: app)
    }
}

This makes tests readable and maintainable across the project.

Recording & Debugging UI Tests

Xcode Test Recorder

Xcode can record your interactions and generate test code automatically. Open your UI test file, place your cursor inside a test method, click the red Record button, interact with the app in the simulator, then click Stop to see the generated code.

The recorder generates code like:

func testRecordedFlow() {
    let app = XCUIApplication()
    app.launch()
    app.textFields["searchField"].tap()
    app.textFields["searchField"].typeText("hello")
    app.buttons["searchButton"].tap()
}

The recorder is a starting point. Always refine the generated code with proper waits, assertions, and accessibility identifiers.

Debugging Failed Tests

When a UI test fails, Xcode shows the exact line where failure occurred, a screenshot of the app state at failure, and the view hierarchy. To debug, set a breakpoint in the test, use po app in the console to inspect the element hierarchy, use the Debug View Hierarchy button, and add XCUIDevice.shared.press(.home) to navigate between screens.

Screenshot Attachment

Attach screenshots to test results for debugging:

func testCaptureScreenshot() {
    let screenshot = XCUIScreen.main.screenshot()
    let attachment = XCTAttachment(screenshot: screenshot)
    attachment.name = "Login Screen"
    attachment.lifetime = .keepAlways
    add(attachment)
}

Test Plans

Use Test Plans to organize test execution. Create a Test Plan via File, New, File, Test Plan. Add test targets and specify which tests to include. Configure parallel execution and test repetitions. Set environment variables for different test configurations.

Accessibility Inspector for Testing

Use Xcode Accessibility Inspector to verify accessibility identifiers are set correctly, check that elements are accessible to VoiceOver, identify missing labels or hints, and debug element hierarchy issues in UI tests.

Quiz

1. What does continueAfterFailure = false do in a UI test?

Question 1 options

2. How do you wait for an element to appear in XCUITest?

Question 2 options

3. Why should you use accessibility identifiers instead of button labels?

Question 3 options

4. What is the Page Object pattern used for in UI testing?

Question 4 options

Flashcards

Question

What is the difference between XCUIApplication and XCUIElement?

Answer

XCUIApplication represents the entire app and is used to launch it. XCUIElement represents a single UI element like a button or text field that you interact with.

Question

What does app.launchEnvironment do?

Answer

Sets environment variables that the app can read at launch time, useful for configuring mock APIs or skipping onboarding in tests.

Question

How do you handle system alerts in UI tests?

Answer

Use app.alerts["title"].buttons["Button"].tap() to interact with system alerts like permission dialogs.

Revision Notes

Key Takeaways

  • 1. Always set accessibility identifiers for testable elements
  • 2. Use waitForExistence instead of Thread.sleep
  • 3. Set continueAfterFailure = false for cleaner debugging
  • 4. Apply Page Object pattern for maintainable tests
  • 5. Use launchEnvironment to configure test-specific behavior

Interview Tips

  • Explain the difference between unit tests and UI tests
  • Describe how you handle flaky UI tests with proper waits
  • Discuss the Page Object pattern and its benefits
  • Talk about how you would test a complex multi-screen flow

Cheat Sheet

XCUITest Quick Reference

  • XCUIApplication() represents your app
  • app.launch() starts the app
  • app.buttons["id"].tap() to interact
  • waitForExistence(timeout:) to wait
  • continueAfterFailure = false to stop on first failure
  • .accessibilityIdentifier() to tag elements