同一批 UI 測試在本機能順利通過,換到遠端節點後卻隨機逾時,常見原因往往不是業務程式碼,而是模擬器殘留狀態、分片邊界變動或並行度過高。可靠的測試矩陣應確保每次執行都能回答三個問題:使用了哪個執行階段、哪些測試案例被分在同一組,以及失敗現場儲存在哪裡。以下將使用 xcodebuild 與 simctl 建立一套可重複執行的流程。
先定義矩陣邊界
不要一開始就遍歷所有裝置與系統版本。先選定一個主要執行階段和一款具代表性的裝置,建立穩定基準後,再逐步加入相容性組合。執行前應記錄 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。重複使用相同路徑,會覆寫最有價值的首次失敗現場。發生失敗後,只重新執行對應分片一次,不要立刻重跑整個矩陣。
第一次失敗、第二次通過,通常表示等待條件、動畫、網路相依性或狀態競爭不穩定。若兩次都在相同步驟失敗,則較可能是確定性的功能退化。若兩次失敗的位置不同,應優先檢查節點資源及測試之間的共享狀態。
上線前可依下列順序檢查:
- 執行階段與裝置類型均來自目前節點的查詢結果。
- 每個分片均使用獨立 UDID 與獨立結果目錄。
- 編譯產物只產生一次,各分片不重複建置。
- 測試清單具有版本記錄,執行前不會隨機重新排序。
- 首次失敗的結果不會被重跑結果覆寫。
- 中斷後只刪除本次建立的模擬器。
常見問題
一台雲端 Mac 適合同時執行多少個 iOS 模擬器?
建議先從兩個分片開始,再依記憶體壓力、CPU 使用率、啟動時間與測試波動調整。若出現記憶體交換或啟動逾時,就應降低並行數。
為什麼每個測試分片都要使用獨立模擬器?
獨立模擬器能隔離應用程式資料、權限、剪貼簿與背景程序,避免分片互相污染,也能在完成後直接刪除重建。
測試失敗後需要重跑整套測試嗎?
不需要。先保存首次失敗的 xcresult,再只重跑失敗分片一次;若第二次通過,應標記為疑似不穩定測試並比較兩份結果。
ArmMacs 雲端 Mac
依專案週期使用獨享實體節點
選擇固定晶片、記憶體、儲存空間、租期與在售節點;庫存可用時即可進入交付流程。