工程指南

在雲端 Mac 建立可重現的 iOS 模擬器測試矩陣

在雲端 Mac 建立可重現的 iOS 模擬器測試矩陣

同一批 UI 測試在本機能順利通過,換到遠端節點後卻隨機逾時,常見原因往往不是業務程式碼,而是模擬器殘留狀態、分片邊界變動或並行度過高。可靠的測試矩陣應確保每次執行都能回答三個問題:使用了哪個執行階段、哪些測試案例被分在同一組,以及失敗現場儲存在哪裡。以下將使用 xcodebuildsimctl 建立一套可重複執行的流程。

先定義矩陣邊界

不要一開始就遍歷所有裝置與系統版本。先選定一個主要執行階段和一款具代表性的裝置,建立穩定基準後,再逐步加入相容性組合。執行前應記錄 Xcode 版本、macOS 版本、執行階段識別碼、提交編號及測試清單。裝置名稱不適合作為唯一鍵,因為可以同時存在多個同名模擬器,因此應儲存 UDID。

維度 基準選擇 擴充條件
系統執行階段 目前專案支援的主要版本 發布前涵蓋最低支援版本
裝置類型 一種常見螢幕尺寸 涉及版面配置適配時增加尺寸
測試分片 依測試類別固定分組 單組執行時間過長時再拆分
並行數 從兩個分片開始 資源曲線穩定後逐步增加

測試矩陣的目的不是堆疊最多的組合,而是讓同一筆提交在相同輸入下產生可解釋的結果。

建立一次性模擬器基準

先查詢節點上實際存在的執行階段與裝置類型,不要在指令碼中寫死版本號。

xcrun simctl list runtimes
xcrun simctl list devicetypes
xcrun simctl list devices available

從輸出中選出對應識別碼,再為每個分片建立獨立裝置。名稱僅供辨識,後續指令一律使用 UDID。

RUNTIME_ID="com.apple.CoreSimulator.SimRuntime.iOS-XX-X"
DEVICE_TYPE_ID="com.apple.CoreSimulator.SimDeviceType.iPhone-XX"

UDID=$(xcrun simctl create "ui-shard-01" "$DEVICE_TYPE_ID" "$RUNTIME_ID")
xcrun simctl boot "$UDID"
xcrun simctl bootstatus "$UDID" -b

不要重複使用開發人員日常除錯的模擬器。測試可能會變更權限、地區、鍵盤、App 資料及背景工作。使用一次性裝置,可將這些狀態限制在單次執行期間。若測試案例依賴相機、通知或定位權限,應在測試準備階段明確設定,不能假設上一次的授權仍然有效。

編譯一次,再依清單分片

測試矩陣不應讓每個分片重複編譯。先執行 build-for-testing 產生統一的測試產物,再由各分片呼叫 test-without-building

DERIVED_PATH="$PWD/.derived-test"

xcodebuild build-for-testing \
  -workspace App.xcworkspace \
  -scheme AppUITests \
  -destination "generic/platform=iOS Simulator" \
  -derivedDataPath "$DERIVED_PATH"

XCTESTRUN=$(find "$DERIVED_PATH" -name "*.xctestrun" -print -quit)

分片清單應提交至儲存庫,避免依字母順序或執行時間臨時拆分,導致每次執行的邊界不同。例如,可將登入與登出放在同一組,離線與同步放在另一組。執行時應停用 Xcode 內建的第二層並行機制,讓並行度完全由外層排程器控制。

xcodebuild test-without-building \
  -xctestrun "$XCTESTRUN" \
  -destination "platform=iOS Simulator,id=$UDID" \
  -only-testing:"AppUITests/AuthenticationTests" \
  -parallel-testing-enabled NO \
  -resultBundlePath "$PWD/results/shard-01.xcresult"

如果各分片的執行時間差異很大,應根據近期多次執行時間的中位數調整清單,但調整後必須固定版本並記錄變更。不要讓排程指令碼在每次執行前自動重新排序,否則失敗結果將無法與上一輪直接比較。

控制並行度並可靠回收

模擬器並非開得越多就越快。每增加一個分片,都會增加啟動程序、圖形服務、測試程序與 App 副本。先以兩個分片觀察記憶體壓力、CPU 飽和度、啟動時間與總執行時間;一旦出現記憶體交換、bootstatus 逾時或測試案例耗時大幅波動,就應立即降低並行度。

無論執行成功、失敗或中斷,指令碼都應進行資源回收。可為已建立的 UDID 註冊結束處理程序,先關機再刪除,避免下一輪誤用舊裝置。

cleanup() {
  xcrun simctl shutdown "$UDID" >/dev/null 2>&1 || true
  xcrun simctl delete "$UDID" >/dev/null 2>&1 || true
}

trap cleanup EXIT INT TERM

不要使用全域刪除指令清空節點上的所有模擬器,因為同一節點可能還有其他工作正在執行。只處理本次執行所建立並記錄的 UDID。若測試需要保留失敗現場,可先封存日誌與結果套件,再刪除裝置。

使用結果套件區分真實失敗與偶發失敗

每個分片都必須使用獨立的 .xcresult 路徑,並同時儲存標準輸出、開始時間、結束時間、結束碼及 UDID。重複使用相同路徑,會覆寫最有價值的首次失敗現場。發生失敗後,只重新執行對應分片一次,不要立刻重跑整個矩陣。

第一次失敗、第二次通過,通常表示等待條件、動畫、網路相依性或狀態競爭不穩定。若兩次都在相同步驟失敗,則較可能是確定性的功能退化。若兩次失敗的位置不同,應優先檢查節點資源及測試之間的共享狀態。

上線前可依下列順序檢查:

  1. 執行階段與裝置類型均來自目前節點的查詢結果。
  2. 每個分片均使用獨立 UDID 與獨立結果目錄。
  3. 編譯產物只產生一次,各分片不重複建置。
  4. 測試清單具有版本記錄,執行前不會隨機重新排序。
  5. 首次失敗的結果不會被重跑結果覆寫。
  6. 中斷後只刪除本次建立的模擬器。

常見問題

一台雲端 Mac 適合同時執行多少個 iOS 模擬器?

建議先從兩個分片開始,再依記憶體壓力、CPU 使用率、啟動時間與測試波動調整。若出現記憶體交換或啟動逾時,就應降低並行數。

為什麼每個測試分片都要使用獨立模擬器?

獨立模擬器能隔離應用程式資料、權限、剪貼簿與背景程序,避免分片互相污染,也能在完成後直接刪除重建。

測試失敗後需要重跑整套測試嗎?

不需要。先保存首次失敗的 xcresult,再只重跑失敗分片一次;若第二次通過,應標記為疑似不穩定測試並比較兩份結果。

ArmMacs 雲端 Mac

依專案週期使用獨享實體節點

選擇固定晶片、記憶體、儲存空間、租期與在售節點;庫存可用時即可進入交付流程。

選擇機型並訂購