同一組 XCUITest 在本機連續通過,移到雲端 Mac 後卻偶爾找不到按鈕、等待逾時或停在啟動畫面,原因通常不只是「機器較慢」。更常見的問題包括執行目標漂移、前一個測試案例留下狀態、使用固定時間等待,以及失敗後未保存足夠的證據。治理時應先固定輸入,再消除隱性相依,最後才考慮重新執行。
先固定每次執行的輸入
先將 Xcode、測試計畫、模擬器目標與建置目錄明確寫入命令,不要依賴圖形介面上次保留的選項。雲端 Mac 可以長期承載工作,同一個工作目錄也可能由不同的流水線共用,因此每次執行都應使用獨立的 DerivedData 與結果套件。
RUN_ID="${CI_RUN_ID:-local-$(date +%s)}"
ARTIFACTS="$PWD/artifacts/$RUN_ID"
DERIVED="$PWD/.derived/$RUN_ID"
mkdir -p "$ARTIFACTS" "$DERIVED"
xcodebuild test \
-workspace Sample.xcworkspace \
-scheme SampleUITests \
-testPlan Smoke \
-destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \
-derivedDataPath "$DERIVED" \
-resultBundlePath "$ARTIFACTS/Smoke.xcresult" \
-parallel-testing-enabled NO
初次治理不穩定的測試案例時,應先停用平行測試,建立循序執行的基準。基準穩定後,再逐組啟用平行測試;否則多個模擬器、測試資料與背景工作同時變動,很難判斷故障究竟來自測試案例還是排程。
啟動前執行最低限度檢查
執行前記錄 xcodebuild -version,並透過 xcrun simctl list devices available 確認目標確實存在。不要只指定裝置名稱而省略系統條件,也不要讓不同 Runner 共用同一個 DerivedData 目錄。
以可觀察狀態取代固定等待
Thread.sleep 看似直接,實際上卻把「頁面準備完成」錯寫成「已經過幾秒」。網路回應較快時,它會浪費時間;動畫或磁碟操作稍慢時,又可能過早結束。
let app = XCUIApplication()
app.launchArguments += ["--ui-testing", "--reset-state"]
app.launch()
let continueButton = app.buttons["onboarding.continue"]
let ready = continueButton.waitForExistence(timeout: 12)
XCTAssertTrue(ready, "Continue button did not appear")
XCTAssertTrue(continueButton.isHittable)
continueButton.tap()
元素應使用穩定的 accessibility identifier,不要依賴本地化文字或檢視階層。若按鈕已經存在但無法點擊,應進一步檢查遮罩、動畫與捲動位置,而不是單純延長逾時時間。
逾時值是故障邊界,不是效能承諾。它應涵蓋正常波動,同時在流程確實卡住時盡快失敗並保存現場資訊。
讓每個測試案例都從已知狀態開始
間歇失敗往往來自前一個測試案例:歡迎頁已被略過、測試資料仍然存在、彈出式視窗只會顯示一次,或背景程序尚未結束。最穩妥的做法,是讓應用程式在測試啟動參數下執行專用的重設邏輯,並為每個測試身分產生唯一資料。
可依照以下邊界分層處理:
| 狀態類型 | 建議做法 | 不建議做法 |
|---|---|---|
| 應用程式偏好設定 | 由啟動參數觸發測試專用重設 | 假設安裝後的狀態永遠為空 |
| 本機資料庫 | 匯入固定測試資料或重建測試資料庫 | 依賴上一個測試案例寫入的資料 |
| 伺服器端資料 | 使用本次執行專屬的唯一命名空間 | 讓多個工作共用固定記錄 |
| 系統權限 | 在測試套件執行前統一準備並驗證 | 在任意測試案例中臨時處理 |
| 應用程式程序 | 測試案例結束後明確終止 | 假設當機後會自動復原 |
可在測試套件層級的清理程序中終止應用程式;只有確實需要驗證首次安裝流程時,才執行解除安裝。頻繁清除整個模擬器不但會增加執行時間,也可能將環境問題掩蓋成「清理後即可通過」。
xcrun simctl terminate booted "$APP_BUNDLE_ID" 2>/dev/null || true
將重新執行變成分類工具
自動重新執行不能取代通過條件。合理的策略是在首次失敗後只重跑一次,並分別保存 attempt-1.xcresult 與 attempt-2.xcresult。若兩次都失敗,應優先視為穩定可重現的缺陷;若第一次失敗、第二次通過,則標記為不穩定測試案例,並繼續調查等待條件、共用狀態與資源競爭。
不要只保留最終結束碼
每次失敗至少應收集以下內容:
- 獨立的結果套件與測試日誌;
- 失敗步驟的螢幕截圖與介面階層;
- Xcode 版本、目標模擬器與啟動參數;
- 本次執行識別碼、測試案例名稱及起訖時間;
- 應用程式結束狀態與相關系統日誌片段。
結果套件的路徑必須包含執行識別碼,以免平行工作互相覆寫。日誌中的權杖、憑證與業務資料,應在上傳前進行遮蔽處理。
從單一測試案例重現到穩定驗收
先使用 -only-testing 連續執行目標測試案例,以減少無關變因;接著執行其所屬的測試類別,最後再回到完整測試套件。若只有完整測試套件會失敗,通常表示存在順序相依、共用資料或資源競爭。
xcodebuild test \
-workspace Sample.xcworkspace \
-scheme SampleUITests \
-destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \
-only-testing:SampleUITests/CheckoutTests/testSubmit \
-parallel-testing-enabled NO
驗收時不要只看「單次通過」。應連續執行數輪,確認不存在固定的順序相依,並檢查總執行時間是否因等待上限過大而明顯增加。恢復平行測試後,還要驗證不同分片不會操作同一筆測試記錄或同一個輸出目錄。
最終目標不是把失敗率藏在重試機制後面,而是讓每次執行都具備確定的輸入、明確的等待條件、隔離的狀態與完整的證據。做到這四點,雲端 Mac 上的 XCUITest 才能從「偶爾通過」轉變為可供合併與發布決策採用的工程訊號。
常見問題
XCUITest 失敗後可以自動重跑嗎?
可以重跑一次協助分類,但不能用無限重試遮蔽問題。第一次失敗與重跑結果都要保存;只有重跑才通過的案例,應列入不穩定測試清單持續修正。
為什麼 UI 測試不應依賴固定 sleep?
固定時間無法證明介面已準備完成,也會在介面提早完成時浪費時間。應等待元素存在、可操作或文字符合預期,並設定明確逾時。
為下一條開發流程選擇雲端 Mac
比較三種固定配置與四個亞洲節點,依實際工作負載選擇租用週期。每筆租用都對應一台獨享實體機,不是虛擬機器。