AI Skill Report Card
Writing iOS Automated Tests
Quick Start15 / 15
SWIFTimport XCTest final class LoginFlowUITests: XCTestCase { var app: XCUIApplication! override func setUpWithError() throws { continueAfterFailure = false app = XCUIApplication() app.launchArguments = ["UI-Testing"] app.launch() } func testSuccessfulLogin() throws { let emailField = app.textFields["emailTextField"] let passwordField = app.secureTextFields["passwordTextField"] let loginButton = app.buttons["loginButton"] XCTAssertTrue(emailField.waitForExistence(timeout: 5)) emailField.tap() emailField.typeText("qa@example.com") passwordField.tap() passwordField.typeText("ValidPass123!") loginButton.tap() let homeTitle = app.staticTexts["homeScreenTitle"] XCTAssertTrue(homeTitle.waitForExistence(timeout: 5)) } }
Run with cmd+U in Xcode or xcodebuild test -scheme YourScheme -destination 'platform=iOS Simulator,name=iPhone 15'.
Recommendation▾
Add an example showing a flaky test anti-pattern vs fixed version for contrast
Workflow14 / 15
Progress:
- Step 1: Identify the manual test case and its preconditions, steps, and expected result
- Step 2: Confirm accessibility identifiers exist on relevant UI elements (request from dev if missing)
- Step 3: Choose test type — XCTest (unit/logic) vs XCUITest (UI/flow)
- Step 4: Write
setUp/tearDownto establish consistent state (launch args, mock data, reset state) - Step 5: Translate each manual step into an action + assertion pair
- Step 6: Add explicit waits (
waitForExistence) instead ofsleep() - Step 7: Run test 3x locally to check for flakiness
- Step 8: Add to test plan / CI scheme and tag (smoke, regression, etc.)
Step 1: Map manual case to automated structure
Take the manual test case and break it into Given/When/Then:
- Given: preconditions (logged out, cart has item, airplane mode on)
- When: user actions (tap, swipe, type, scroll)
- Then: expected outcome (element appears, value changes, navigation occurs)
Step 2: Ensure testability
Request accessibility identifiers from developers rather than relying on label text (which changes with localization):
SWIFT// Dev side loginButton.accessibilityIdentifier = "loginButton"
If devs can't add identifiers immediately, fall back to app.buttons["Log In"] temporarily and flag as tech debt.
Step 3: Unit vs UI test decision
| Use XCTest (unit) when... | Use XCUITest (UI) when... |
|---|---|
| Testing a function, validator, or model | Testing a user-facing flow |
| Testing API/network response handling (mocked) | Testing navigation, gestures, visual state |
| Fast feedback needed, no simulator required | Verifying end-to-end user journeys |
Step 4: Structure for reliability
- One behavior per test method — don't chain unrelated assertions
- Use
launchArguments/launchEnvironmentto inject test mode (mock APIs, seeded data) - Reset app state in
setUpWithError, not relying on test execution order
Recommendation▾
Include guidance on handling async/await or Combine-based network mocking in XCUITest context
Examples18 / 20
Example 1: Input: Manual test case — "Verify user can add item to cart and see updated badge count" Output:
SWIFTfunc testAddItemUpdatesCartBadge() throws { let product = app.cells["product_123"] XCTAssertTrue(product.waitForExistence(timeout: 5)) product.tap() app.buttons["addToCartButton"].tap() let cartBadge = app.staticTexts["cartBadgeCount"] XCTAssertTrue(cartBadge.waitForExistence(timeout: 5)) XCTAssertEqual(cartBadge.label, "1") }
Example 2: Input: Manual test case — "Verify form validation shows error for invalid email" Output:
SWIFTfunc testInvalidEmailShowsError() throws { let emailField = app.textFields["emailTextField"] emailField.tap() emailField.typeText("not-an-email") app.buttons["submitButton"].tap() let errorLabel = app.staticTexts["emailErrorLabel"] XCTAssertTrue(errorLabel.waitForExistence(timeout: 3)) XCTAssertEqual(errorLabel.label, "Please enter a valid email address") }
Example 3: Input: Business logic — "Discount should apply only to orders over $50" Output:
SWIFTfunc testDiscountAppliesAboveThreshold() { let order = Order(subtotal: 60.0) XCTAssertEqual(order.applyDiscount(), 54.0) // 10% off let smallOrder = Order(subtotal: 30.0) XCTAssertEqual(smallOrder.applyDiscount(), 30.0) // no discount }
Recommendation▾
Consider adding a brief section on CI integration specifics (fastlane, GitHub Actions) for completeness
Best Practices
- Use
waitForExistence(timeout:)for every assertion on a newly-appearing element — never assume instant rendering - Inject mock/stubbed network responses via launch environment so tests don't depend on live backends
- Name tests descriptively:
test<Scenario>_<ExpectedResult> - Group tests into suites by feature (LoginTests, CheckoutTests) and tag smoke vs regression
- Keep a
Page Objectlayer (helper structs wrappingXCUIApplicationqueries) so UI changes require updating one file, not every test - Run new tests repeatedly (5-10x) before merging to catch flakiness early
- Capture screenshots on failure using
XCTAttachmentfor easier debugging in CI
Common Pitfalls
- Don't use
sleep()for timing — it's slow and still flaky; always usewaitForExistence - Don't rely on visible text labels for element lookup if localization is in scope — use identifiers
- Don't let tests depend on execution order or shared mutable state between test methods
- Don't test against production/live data — always use a seeded/mock environment
- Don't write one giant test covering an entire user journey — split into focused, independently-diagnosable tests
- Don't ignore intermittent failures as "just flaky" — investigate; it's often a missing wait condition