工程實務

雲端 Mac 治理 XCUITest 間歇失敗:等待、狀態隔離與證據保存

雲端 Mac 治理 XCUITest 間歇失敗:等待、狀態隔離與證據保存

同一組 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.xcresultattempt-2.xcresult。若兩次都失敗,應優先視為穩定可重現的缺陷;若第一次失敗、第二次通過,則標記為不穩定測試案例,並繼續調查等待條件、共用狀態與資源競爭。

不要只保留最終結束碼

每次失敗至少應收集以下內容:

  1. 獨立的結果套件與測試日誌;
  2. 失敗步驟的螢幕截圖與介面階層;
  3. Xcode 版本、目標模擬器與啟動參數;
  4. 本次執行識別碼、測試案例名稱及起訖時間;
  5. 應用程式結束狀態與相關系統日誌片段。

結果套件的路徑必須包含執行識別碼,以免平行工作互相覆寫。日誌中的權杖、憑證與業務資料,應在上傳前進行遮蔽處理。

從單一測試案例重現到穩定驗收

先使用 -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 mini

為下一條開發流程選擇雲端 Mac

比較三種固定配置與四個亞洲節點,依實際工作負載選擇租用週期。每筆租用都對應一台獨享實體機,不是虛擬機器。

選擇配置並租用