MiniRent 工程指南

雲端 Mac 用 simctl privacy 建立 iOS 權限回歸測試

雲端 Mac 用 simctl privacy 建立 iOS 權限回歸測試

同一組 iOS 介面測試在開發機上可以通過,移到雲端 Mac 後卻偶爾卡在權限提示框,通常不是業務程式碼隨機失效,而是模擬器保留了前一次任務的 TCC 權限狀態。有人點過「允許」,後續測試案例就不會看到首次詢問;有人點過「拒絕」,依賴聯絡人或照片的流程又會直接進入失敗分支。要讓測試保持穩定,關鍵不是更頻繁地重試,而是在每個測試案例開始前明確宣告權限前置條件。

將權限狀態視為測試輸入

權限測試至少要涵蓋三種狀態:尚未決定、已經允許、已經拒絕。它們分別對應不同的產品流程,不能全都混在同一個「重新安裝 App」步驟中處理。

目標狀態 simctl 動作 應驗證的行為
尚未決定 reset App 發出請求,系統顯示權限提示
已經允許 grant 功能直接繼續,不再重複詢問
已經拒絕 revoke App 顯示降級說明或設定引導

先確認目前的執行環境支援目標服務。不同系統版本能控制的服務可能不同,因此不要只憑經驗將服務名稱寫死後直接提交:

xcrun simctl privacy help
xcrun simctl list devices available

常見服務包括 contactsphotosphotos-addlocationmicrophonecalendar。測試指令碼應以目前 Xcode 所附 simctl 的說明輸出為準。

權限狀態屬於模擬器裝置,而不屬於程式碼儲存庫。只清除建置目錄不會清除 TCC 資料,刪除並重新安裝 App 也不應視為可靠的權限重設方式。

準備專用模擬器與 App

CI 不應隨意使用 booted 指向任意已啟動的裝置。平行任務可能同時找到同一台模擬器,導致安裝、啟動與權限修改互相覆蓋。更穩妥的方式是為任務指派明確的 UDID,並將它作為指令碼參數傳入。

必須先安裝 App,grantrevoke 才能對實際的 Bundle Identifier 生效。Bundle Identifier 也不要從 Scheme 名稱推斷,可以直接從建置產物讀取:

APP_PATH="$1"
UDID="$2"
BUNDLE_ID=$(/usr/libexec/PlistBuddy -c "Print :CFBundleIdentifier" \
  "$APP_PATH/Info.plist")

xcrun simctl boot "$UDID" 2>/dev/null || :
xcrun simctl bootstatus "$UDID" -b
xcrun simctl install "$UDID" "$APP_PATH"
printf '%s
' "$BUNDLE_ID"

如果測試對象是擴充功能,擴充功能會有獨立的 Bundle Identifier。應分別讀取擴充功能的 Info.plist,不要將主 App 的識別碼套用到所有權限命令。

使用指令碼固定三種前置狀態

將狀態切換集中在一個函式中,比散落在流水線設定裡更容易審查。修改權限前先終止 App,避免程序仍持有舊狀態或正在顯示提示框。

set -euo pipefail

UDID="$1"
BUNDLE_ID="$2"
SERVICE="$3"
STATE="$4"

xcrun simctl terminate "$UDID" "$BUNDLE_ID" 2>/dev/null || :

case "$STATE" in
  undecided)
    xcrun simctl privacy "$UDID" reset "$SERVICE" "$BUNDLE_ID"
    ;;
  authorized)
    xcrun simctl privacy "$UDID" grant "$SERVICE" "$BUNDLE_ID"
    ;;
  denied)
    xcrun simctl privacy "$UDID" revoke "$SERVICE" "$BUNDLE_ID"
    ;;
  *)
    printf 'Unknown permission state: %s
' "$STATE" >&2
    exit 64
    ;;
esac

執行後再啟動 App 或執行測試。例如,針對已允許聯絡人存取的情境,可以先呼叫指令碼,再只執行對應的測試類別:

./set-permission.sh "$UDID" "$BUNDLE_ID" contacts authorized

xcodebuild test \
  -scheme PermissionTests \
  -destination "platform=iOS Simulator,id=$UDID" \
  -only-testing:PermissionUITests/ContactsAuthorizedTests \
  -resultBundlePath Artifacts/ContactsAuthorized.xcresult

reset 代表回到尚未決定,而不是拒絕。如果業務程式碼將這兩種狀態合併處理,測試應直接揭露這個設計問題。

分開測試系統提示與業務分支

系統提示屬於系統介面,按鈕定位容易受到語言、執行環境版本與提示順序影響。不要讓所有權限測試都依賴點擊提示框。更合理的分層方式是:大多數測試透過 grantrevoke 驗證業務分支,只保留少量測試案例驗證首次請求。

首次請求測試案例

先執行 reset,再啟動 App 並觸發權限請求。測試案例只檢查提示是否出現,以及使用者選擇後 App 是否進入預期頁面。若需要操作系統介面,應使用獨立的系統 App 物件,並避免依賴固定座標。

已允許與已拒絕測試案例

這兩類測試案例都不應看到提示框。已允許狀態要驗證功能可以正常讀取資料;已拒絕狀態則要驗證 App 不會重複循環請求,也不會停留在無限載入狀態。若有設定引導,只需驗證按鈕與說明,不要在自動化任務中實際修改系統設定。

位置權限還需要區分 App 使用期間與持續授權等業務語意。simctl privacy 可以準備基礎狀態,但無法取代所有實機行為,因此相關結論仍應保留實機驗收。

隔離、蒐證與失敗排查

每個測試類別結束後,可以對對應服務執行 reset,但更重要的是讓下一項測試自行建立狀態,而不是依賴上一項測試成功清理。平行執行時,一台模擬器只交給一個任務;若需要並行,就建立多台裝置,不要共用 UDID。

失敗時至少要保存 .xcresult、測試日誌、模擬器系統日誌與目前的裝置清單。如果預期狀態沒有生效,請依照以下順序檢查:

  1. Bundle Identifier 是否來自實際安裝的 App 或擴充功能。
  2. 目前執行環境是否支援該服務名稱。
  3. 修改狀態前是否已終止 App。
  4. 目標 UDID 是否與 xcodebuild 使用的裝置一致。
  5. 是否有另一個平行任務正在操作同一台模擬器。
  6. 測試程式碼是否快取了舊的授權結果。

不要將「清除整台模擬器」設為預設修復方式。這會顯著增加任務時間,也可能掩蓋狀態管理缺陷。只有當裝置持續出現無法解釋的系統狀態時,才重建該裝置,並保留此前的日誌。

在 MiniRent 的雲端 Mac 上執行這類任務時,機型與節點只會影響任務的放置方式;測試指令碼仍應透過 UDID、Bundle Identifier、服務名稱與目標狀態完整描述環境,並在控制台確認目前可選的設定。如此一來,同一套權限迴歸測試既能在臨時除錯任務中重現,也能穩定納入持續整合。

常見問題

simctl privacy 可以完全取代系統權限提示測試嗎?

不可以。它適合準備可重現的權限狀態,但實際提示文字、按鈕操作與返回流程仍需少量端對端介面測試。

每個權限案例前都要清除整個模擬器嗎?

通常不用。先終止應用程式,再針對其套件識別碼執行 reset、grant 或 revoke;只有整體狀態失去可信度時才重建模擬器。

獨享實體設備

使用雲端 Mac mini 執行下一項開發或建置任務

可從兩種 M4 設定、四種租用週期與五個可租用節點中選擇,實際可用狀態以控制台即時回傳為準。

選擇設定並租用