← Back to blog

UI Tests for iOS Apps — Basics

An introduction to end-to-end testing in iOS applications using XCTest framework and UI automation.

UI tests represent the top tier of the testing pyramid, verifying functionality through actual user interface interactions. Also called "End-to-End" (E2E) tests, they simulate real user workflows from button clicks to server responses.

The primary trade-off with UI tests is execution speed — they run considerably slower than unit or integration tests. However, they provide the closest approximation to genuine user experience.

How UI Tests Work

UI tests use the XCTest framework by extending test classes with XCTestCase. Unlike unit tests, UI tests require actual interface interaction:

let app = XCUIApplication()
app.launch()

When running, two applications are installed: the target application and a simulator that replicates user input based on your test code.

Accessing UI Elements

Rather than using pixel coordinates, XCTest allows element access through properties like text content or button titles:

let nameTextField = app.textFields["Name"]

Recording Tests

Xcode includes a "Record UI Tests" button that automatically captures your manual interactions with the app, generating test code automatically. However, this may require manual cleanup.

Example of a recorded test:

func testStartAndSelectNameTextField() throws {
    let app = XCUIApplication()
    app.launch()

    app.textFields["Name"].tap()

    let vKey = app.keys["V"]
    vKey.tap()

    let iKey = app.keys["i"]
    iKey.tap()

    let cKey = app.keys["c"]
    cKey.tap()

    let tKey = app.keys["t"]
    tKey.tap()

    let oKey = app.keys["o"]
    oKey.tap()

    app.keys["r"].tap()
}

Using Assertions

Tests should verify expected outcomes:

func testSaveButtonEnabledAfterFillAllTextFields() throws {
    let app = XCUIApplication()
    app.launch()

    app.textFields["Name"].tap()
    app.typeText("Test")

    app.textFields["Surname"].tap()
    app.typeText("Test")

    app.textFields["Email"].tap()
    app.typeText("Test")

    app.textFields["Phone"].tap()
    app.typeText("Test")

    XCTAssertTrue(app.buttons["Save"].isEnabled)
}

The Accessibility Identifier Solution

A critical best practice involves using accessibilityIdentifier properties to decouple tests from UI text that may change:

Button {
    // Action
} label: {
    Text("Guardar")
}
.accessibilityIdentifier("SaveButton")

This ensures tests remain robust even when displayed text updates from external sources. Instead of referencing the button by its label text, you reference it by identifier:

XCTAssertTrue(app.buttons["SaveButton"].isEnabled)