UI Test Development for iOS Application (XCUITest)
XCUITest is instrumental testing: test runs on real simulator or device, controls app via Accessibility API, taps buttons and checks correct text appears on screen. Much slower than unit tests, but covers what unit tests can't — navigation, data display, input reaction.
Main Patterns and Common Errors
Most common XCUITest problem — fragility. Test searches element by label, designer changes button text — test fails. Correct solution: accessibilityIdentifier.
// In app code
button.accessibilityIdentifier = "loginButton"
// In test
let loginButton = app.buttons["loginButton"]
XCTAssertTrue(loginButton.exists)
loginButton.tap()
accessibilityIdentifier not displayed to user and doesn't change on localization. Only reliable way to address elements.
Second antipattern: sleep(3) instead of element wait. Test with hardcoded pauses unstable and slow.
// Bad
sleep(3)
XCTAssertTrue(app.staticTexts["Welcome"].exists)
// Correct
let welcomeText = app.staticTexts["Welcome"]
XCTAssertTrue(welcomeText.waitForExistence(timeout: 5))
waitForExistence(timeout:) blocks thread until element appears or timeout expires. Test finishes faster on success and doesn't depend on CI machine speed.
Page Object Pattern
With 20+ test scenarios duplication becomes problem. Page Object isolates UI interactions:
struct LoginScreen {
private let app: XCUIApplication
var emailField: XCUIElement { app.textFields["emailInput"] }
var passwordField: XCUIElement { app.secureTextFields["passwordInput"] }
var loginButton: XCUIElement { app.buttons["loginButton"] }
var errorLabel: XCUIElement { app.staticTexts["errorMessage"] }
func login(email: String, password: String) {
emailField.tap()
emailField.typeText(email)
passwordField.tap()
passwordField.typeText(password)
loginButton.tap()
}
}
// Test reads as scenario, not as set of UI instructions
func testLoginWithInvalidCredentials() {
let loginScreen = LoginScreen(app: app)
loginScreen.login(email: "[email protected]", password: "badpass")
XCTAssertTrue(loginScreen.errorLabel.waitForExistence(timeout: 3))
}
Backend Mocking
UI tests shouldn't depend on real server. Two approaches:
Launch arguments — app in test mode loads mock data:
// setUp
app.launchArguments = ["--uitesting", "--mock-auth-success"]
// In AppDelegate / SceneDelegate
if ProcessInfo.processInfo.arguments.contains("--uitesting") {
setupMockDependencies()
}
Local HTTP mock — Swifter or GCDWebServer runs local server in test host. More realistic, but more complex setup.
Screenshot Tests
XCTAttachment saves screenshots at test moment for later analysis. For UI snapshot testing (detect visual regressions) use SnapshotTesting from Point-Free — compares PNG snapshots with baselines.
CI Execution
- name: Run UI Tests
run: |
xcodebuild test \
-scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 15 Pro,OS=17.2' \
-resultBundlePath TestResults.xcresult \
-testPlan UITests
Parallel execution via -parallel-testing-enabled YES speeds up large suite. On Firebase Test Lab — matrix of physical devices for final run before release.
What We Cover
- Critical user flows: registration, login, onboarding, payment
- Navigation edge cases: deep link, push notification tap, force close and return
- Accessibility: VoiceOver via
XCUIApplication().activate()in accessibility mode
Timeframe: 3–5 days for creating basic suite for critical flows with Page Object structure and CI integration.







