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)