BookaMac 工程紀錄

雲端 Mac 用 simctl 建立可重複的 iOS 定位回歸測試

雲端 Mac 用 simctl 建立可重複的 iOS 定位回歸測試

外送地址選擇、門市範圍判斷、地理圍欄提醒等功能,往往在開發用 Mac 上操作幾下就能通過,進入無人值守的 CI 流程後卻開始偶發失敗。問題通常不在定位演算法,而是模擬器沿用了上一個工作所留下的座標、權限,或仍在執行的路線。若要讓雲端 Mac 上的定位測試可重複執行,就必須把「位置」視為需要明確建立與銷毀的測試輸入,而不是模擬器的環境背景。

先拆分三層驗證目標

不要讓單一 UI 測試同時負責座標計算、系統定位串接和介面斷言。更穩定的架構可分為三層:

  1. 使用一般單元測試驗證距離、區域和狀態轉換,直接傳入預先建立的座標。
  2. 使用少量模擬器測試,確認系統定位資料確實進入應用程式,並觸發預期頁面。
  3. 使用實機驗收背景喚醒、訊號飄移、耗電量和感測器相關行為。

可以先為業務層定義範圍很窄的輸入邊界,例如讓定位服務只輸出座標、時間與授權狀態。圍欄判斷不要直接散落在 View Controller 裡。如此一來,絕大多數邊界條件都不必啟動模擬器,只有系統串接部分需要依賴 simctl location

模擬座標可以證明應用程式如何處理特定位置,卻無法證明真實無線環境會以相同節奏傳送位置事件。

為每個工作準備獨立模擬器

先固定 Xcode 路徑,再列出目前機器實際安裝的裝置類型與執行階段。不要把範例中的執行階段識別碼直接永久寫死,因為升級 Xcode 後,識別碼可能改變。

sudo xcode-select -s /Applications/Xcode.app
xcrun simctl list devicetypes
xcrun simctl list runtimes

CI 流程應建立獨立的 CoreSimulator 裝置集,並將產生的 UDID 儲存在目前工作中。以下裝置類型和執行階段識別碼,需要替換成上一個指令實際傳回的值。

set -euo pipefail

DEVICE_SET="$PWD/.simulator-location-tests"
DEVICE_TYPE="com.apple.CoreSimulator.SimDeviceType.iPhone-16"
RUNTIME="com.apple.CoreSimulator.SimRuntime.iOS-18-0"

rm -rf "$DEVICE_SET"
UDID="$(xcrun simctl --set "$DEVICE_SET" create GeoTest "$DEVICE_TYPE" "$RUNTIME")"
xcrun simctl --set "$DEVICE_SET" boot "$UDID"
xcrun simctl --set "$DEVICE_SET" bootstatus "$UDID" -b

與反覆清除共用模擬器相比,獨立裝置集更容易除錯:失敗工作的裝置目錄可以暫時保留,成功工作則可直接刪除。平行工作也不會爭用同一部已啟動的裝置。

注入固定座標與權限

固定座標適合驗證「位於區域內」「越過邊界」「遠離目標點」等確定性條件。應先安裝應用程式、預先設定權限並指定座標,再啟動測試目標。如果等到應用程式啟動後才變更權限,第一個畫面可能已經記錄未授權狀態。

BUNDLE_ID="com.example.LocationDemo"

xcrun simctl --set "$DEVICE_SET" privacy "$UDID" grant location "$BUNDLE_ID"
xcrun simctl --set "$DEVICE_SET" location "$UDID" set 1.290270,103.851959

xcodebuild test \
  -workspace LocationDemo.xcworkspace \
  -scheme LocationDemo \
  -destination "platform=iOS Simulator,id=$UDID" \
  -resultBundlePath "$PWD/TestResults/LocationTests.xcresult"

邊界測試不要只設定一個點。至少要準備區域內、邊界附近和區域外三組座標,並讓測試資料直接說明預期結果,而不是用 case1case2 之類的名稱隱藏含意。

情境 輸入設計 建議斷言
區域內 與中心的距離明顯小於閾值 狀態穩定維持在區域內
邊界附近 在閾值兩側各放一個點 比較規則與取整方式一致
區域外 與中心的距離明顯大於閾值 不觸發區域內動作
權限關閉 撤銷定位授權 顯示可恢復的引導狀態

如果應用程式會快取最後一次位置,還需要在每個測試案例執行前清除應用程式資料,或透過測試啟動參數停用持久快取,否則新座標可能會被舊值覆蓋。

使用 GPX 驗證移動順序

連續移動應使用簡短且易讀的 GPX 檔案。路線只保留能觸發關鍵狀態變化的途經點,不要複製整段真實軌跡。以下檔案會從區域外移動至區域內:

<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1" creator="location-ci">
  <wpt lat="1.320000" lon="103.820000">
    <name>outside</name>
  </wpt>
  <wpt lat="1.290270" lon="103.851959">
    <name>inside</name>
  </wpt>
</gpx>

執行路線時,斷言應以事件順序為依據,而不是依賴精確到秒的等待時間。

xcrun simctl --set "$DEVICE_SET" location "$UDID" start "$PWD/Fixtures/enter-region.gpx"
xcodebuild test \
  -workspace LocationDemo.xcworkspace \
  -scheme LocationRouteTests \
  -destination "platform=iOS Simulator,id=$UDID"
xcrun simctl --set "$DEVICE_SET" location "$UDID" stop

應用程式端可以記錄去識別化後的狀態序列,例如 outside → approaching → inside,並為每次變化附上單調遞增時間。不要將完整座標寫入長期日誌;失敗證據只需保留測試案例名稱、預期區域、實際狀態,以及限制小數位數的數值。

清理狀態並保留失敗證據

清理作業不能只在成功分支執行。指令碼開始後應立即註冊結束處理程序,停止路線、清除定位並關閉裝置;是否刪除裝置集,則依測試結果決定。

cleanup() {
  xcrun simctl --set "$DEVICE_SET" location "$UDID" stop 2>/dev/null || true
  xcrun simctl --set "$DEVICE_SET" location "$UDID" clear 2>/dev/null || true
  xcrun simctl --set "$DEVICE_SET" shutdown "$UDID" 2>/dev/null || true
}
trap cleanup EXIT

測試失敗時,應封存 xcresult、應用程式日誌、使用過的 GPX 檔案、裝置與執行階段識別碼,以及測試開始前的權限狀態。螢幕截圖只能協助判斷,不能取代結構化斷言。如果同一個座標反覆失敗,應先確認裝置集是否確實隔離、Bundle ID 是否正確,以及權限是否在啟動前授予,之後再檢查業務計算。

一套穩定的定位測試流程,最終應做到:測試輸入清楚易讀、模擬器狀態僅使用一次、路線必定停止,且業務邏輯能脫離系統定位進行單元測試。如此一來,發生失敗時,團隊看到的會是明確的狀態差異,而不是一句無法重現的「定位偶爾不準」。

常見問題

simctl 注入定位後,可以取代真實裝置測試嗎?

不可以。它適合驗證定位資料進入應用程式後的狀態轉換、畫面與業務規則;背景喚醒、真實訊號漂移、耗電與感測器行為仍需在真實裝置上驗收。

為什麼定位測試單獨執行會通過,進入完整 CI 後卻失敗?

常見原因是共用模擬器保留了上一次的座標、權限或 GPX 路線。每個工作應使用獨立裝置集,並在測試前後明確清除狀態。

固定座標與 GPX 路線應如何分工?

固定座標用於區域與邊界條件的確定性斷言;GPX 路線用於驗證連續移動和狀態順序,但不應依賴精確到秒的抵達時間。

獨享實體 Mac mini

將已驗證的工作流程部署到持續在線的雲端 Mac

依用途選擇 BookaMac M4 或 BookaMac M4 Pro,下單時請確認地區、租用期限及儲存空間加購項目。

選擇租用方案