UI tests
Hub › iOS › Advanced › UI tests
Goal
Add an XCUITest target that launches the app, signs in, taps a favorite, and navigates to the detail screen. Tests run on the simulator and verify user-visible behavior.
Prerequisites
Create the UI test target
In Xcode: File → New → Target → UI Testing Bundle. Name it ListAppUITests.
XCUITest launches your app and drives it through the Accessibility hierarchy. Every button, label, and cell must have an accessibility identifier for tests to find them.
Add accessibility identifiers
Update your views with .accessibilityIdentifier:
// In the List row:
Button(item.isFavorite ? "★" : "☆") {
vm.toggleFavorite(item)
}
.accessibilityIdentifier(item.isFavorite ? "favorite-on" : "favorite-off")
// In SignInView:
SignInWithAppleButton(...)
.accessibilityIdentifier("sign-in-button")
// Sign out button:
Button("Sign Out") { TokenManager.shared.signOut() }
.accessibilityIdentifier("sign-out-button")UI tests
Create ListAppUITests.swift:
import XCTest
final class ListAppUITests: XCTestCase {
var app: XCUIApplication!
override func setUp() {
continueAfterFailure = false
app = XCUIApplication()
app.launch()
}
func testSignInPromptAppears() {
// The sign-in sheet should appear on first launch
let signInButton = app.buttons["sign-in-button"]
XCTAssertTrue(signInButton.waitForExistence(timeout: 5))
}
func testListLoadsAfterSignIn() {
// Dismiss the sign-in sheet (simulate signed-in state)
let signInButton = app.buttons["sign-in-button"]
if signInButton.exists {
signInButton.tap()
}
// Wait for the list to populate
let list = app.collectionViews.firstMatch
XCTAssertTrue(list.waitForExistence(timeout: 10))
XCTAssertGreaterThan(list.cells.count, 0)
}
func testToggleFavorite() {
// Sign in
let signInButton = app.buttons["sign-in-button"]
if signInButton.exists { signInButton.tap() }
let list = app.collectionViews.firstMatch
XCTAssertTrue(list.waitForExistence(timeout: 10))
// Tap the first favorite button
let favOff = app.buttons["favorite-off"]
if favOff.exists {
favOff.tap()
let favOn = app.buttons["favorite-on"]
XCTAssertTrue(favOn.waitForExistence(timeout: 2))
}
}
func testSignOut() {
// Sign in first
app.buttons["sign-in-button"].tapIfExists()
// Tap sign out
let signOut = app.buttons["sign-out-button"]
XCTAssertTrue(signOut.waitForExistence(timeout: 5))
signOut.tap()
// Sign-in prompt reappears
XCTAssertTrue(app.buttons["sign-in-button"].waitForExistence(timeout: 3))
}
}
extension XCUIElement {
/// Tap the element if it exists and is hittable, within a short timeout.
func tapIfExists(timeout: TimeInterval = 3) {
if waitForExistence(timeout: timeout) && isHittable {
tap()
}
}
}Run UI tests
xcodebuild test \
-scheme ListApp \
-sdk iphonesimulator \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-testPlan ListAppUITestsOr in Xcode: hold the Test button (▶) → choose the UI test plan.
Checkpoint
All UI tests pass: sign-in sheet appears, list loads after sign-in, favorite toggle works, and sign-out returns to the sign-in prompt.
Next: CI/CD with GitHub Actions — automate builds, tests, and archiving.