工程實務

雲端 Mac 用 simctl push 自動驗收 iOS 推播通知

雲端 Mac 用 simctl push 自動驗收 iOS 推播通知

遠端流水線已產出可安裝的 iOS App,但團隊手邊沒有持續連線的測試裝置。此時最容易遺漏的不是編譯錯誤,而是通知權限、前景顯示、深層連結參數與通知服務擴充功能之間的組合問題。simctl push 可將 APNs 格式的負載直接注入 iOS Simulator,適合在雲端 Mac 上進行快速且可重複的提交前驗收。

它無法取代真實的 APNs 傳送測試,卻能在建置階段提早攔截大量用戶端問題。關鍵在於不能只看命令的結束代碼,還必須同時保存 App 狀態、螢幕截圖與統一日誌。

先定義這項測試能證明什麼

simctl push 驗證的是已建置 App 如何處理通知負載。它可以涵蓋通知內容解析、前景與背景代理回呼、分類按鈕、深層連結路由,以及 Notification Service Extension 的處理結果。

它無法驗證伺服器端驗證、裝置權杖有效性、外部網路傳送、APNs 限流,或真實裝置的省電策略。模擬器收到負載,也不代表正式環境的推播鏈路已經打通。

命令成功只代表「已完成本機注入」,不代表「使用者已收到通知」。驗收結論必須以介面、App 日誌與路由結果為依據。

開始前,請確認 App 已宣告通知功能,並在測試流程中完成授權。simctl push 不會代替 App 點擊系統權限彈窗。首次授權可以交由 UI 測試處理,也可以預先在專用測試模擬器中完成,但不能依賴某台長期使用的模擬器狀態。

固定模擬器與 App 輸入

執行平行作業時,不要直接使用 booted。如果同時啟動多台模擬器,它可能會將負載傳送到其他作業。請為每個作業記錄明確的 UDID,並將 App 路徑、Bundle ID 與產物目錄設為輸入。

set -euo pipefail

: "${SIMULATOR_UDID:?Set SIMULATOR_UDID}"
: "${APP_PATH:?Set APP_PATH}"
: "${BUNDLE_ID:?Set BUNDLE_ID}"

ARTIFACTS="${ARTIFACTS:-$PWD/.ci/push-check}"
mkdir -p "$ARTIFACTS"

xcrun simctl bootstatus "$SIMULATOR_UDID" -b
xcrun simctl install "$SIMULATOR_UDID" "$APP_PATH"
xcrun simctl launch "$SIMULATOR_UDID" "$BUNDLE_ID"
xcrun simctl get_app_container "$SIMULATOR_UDID" "$BUNDLE_ID"

bootstatus -b 會等待系統真正完成啟動,比啟動後固定等待數秒更穩定。get_app_container 則是一項低成本的安裝檢查:如果 Bundle ID 錯誤或 App 尚未安裝,作業會在傳送通知前失敗。

需要重新測試首次授權時,應為該作業建立或清除專用模擬器,而不是任意重設共用裝置的所有隱私權設定。如此可避免平行測試互相變更相機、照片或通知權限。

產生並驗證 APNs 負載

負載檔案採用 JSON。建議加入可供搜尋的測試識別碼,以便將螢幕截圖、App 日誌與流水線作業對應起來。

PAYLOAD="$ARTIFACTS/foreground.apns"
TRACE_ID="push-check-${CI_RUN_ID:-local}"

cat > "$PAYLOAD" <<JSON
{
  "Simulator Target Bundle": "$BUNDLE_ID",
  "aps": {
    "alert": {
      "title": "Build completed",
      "body": "Open the result for details"
    },
    "sound": "default",
    "badge": 1,
    "category": "BUILD_RESULT"
  },
  "route": "build/result",
  "trace_id": "$TRACE_ID"
}
JSON

plutil -lint "$PAYLOAD"
PAYLOAD_BYTES="$(wc -c < "$PAYLOAD" | tr -d ' ')"
test "$PAYLOAD_BYTES" -le 4096
xcrun simctl push "$SIMULATOR_UDID" "$BUNDLE_ID" "$PAYLOAD"

4KB 檢查可用來提早發現異常負載,但模擬器不會完整重現伺服器端的所有驗證。App 端也應對 route、識別碼與自訂欄位進行型別檢查,不能因為測試負載可信就直接強制轉型。

如果使用通知服務擴充功能,請加入 "mutable-content": 1,並讓擴充功能寫入統一的結構化日誌。擴充功能的處理時間與遭系統終止的情況仍應依失敗路徑設計,不能假設它一定能完成下載或改寫。

依 App 狀態建立驗收矩陣

同一負載至少必須涵蓋前景、背景與未執行三種狀態。前景通知通常由代理方法決定是否顯示橫幅;因此「沒有橫幅」可能是產品邏輯,也可能是遺漏回呼,必須明確列出每種狀態的預期結果。

情境 操作 必查結果
前景 啟動 App 後注入 代理回呼、頁面內提示、重複事件
背景 返回主畫面後注入 橫幅內容、標記、點擊路由
未執行 終止 App 後注入 冷啟動入口、參數解析、目標頁面
內容擴充 注入含有 mutable-content 的負載 擴充功能日誌、改寫內容、逾時回退
異常欄位 缺少路由或欄位型別錯誤 安全降級、不當機、可診斷日誌

傳送後請保存螢幕截圖,避免流水線只留下「命令成功」這種缺乏實證的結論。

xcrun simctl io "$SIMULATOR_UDID" screenshot \
  "$ARTIFACTS/notification.png"

xcrun simctl spawn "$SIMULATOR_UDID" log show \
  --last 2m \
  --style compact \
  --predicate "process == '$BUNDLE_ID'" \
  > "$ARTIFACTS/application.log"

程序名稱不一定等同於 Bundle ID。更穩妥的做法是在 App 與擴充功能中使用固定的日誌 subsystem,再依 subsystem 查詢。日誌只記錄測試識別碼、狀態與錯誤類型,不要寫入完整通知內文、權杖或使用者資料。

將失敗條件寫入流水線

可靠的通知驗收作業不應只執行 shell 命令,還必須判斷產物是否存在、App 是否當機,以及預期路由是否已記錄。可以讓 App 在測試建置中將已處理的 trace_id 與結果寫入專用檔案,再透過 simctl get_app_container 找到容器並讀取。測試介面只能在內部建置設定中啟用,發佈設定必須關閉。

常見的誤判包括:重複使用已授權的模擬器,因而漏測首次彈窗;使用 booted 導致平行作業互相串線;前景未顯示橫幅卻被判定為注入失敗;只檢查螢幕截圖,卻未驗證點擊後的路由;日誌查詢範圍過大,讀取到前一次作業的記錄。

提交前請依下列順序收尾:

  1. 固定 Xcode、執行階段、裝置型號與模擬器 UDID。
  2. 安裝本次建置產物,不重複使用舊的 App 容器。
  3. 驗證 JSON 語法、位元組數與必要欄位。
  4. 分別執行前景、背景、未執行與異常負載測試案例。
  5. 封存負載、螢幕截圖、App 日誌與唯一測試識別碼。
  6. 將模擬器驗收與真實 APNs 端對端測試分開報告。

完成這些步驟後,推播用戶端邏輯就不再只是依賴人工觀察的功能,而會成為可重複執行、保留證據,並準確定位失敗階段的工程檢查。

常見問題

simctl push 成功是否代表真實 APNs 投遞一定成功?

不是。它只證明負載已注入模擬器,無法驗證伺服器驗證、裝置權杖、網路投遞與 APNs 限制;這些環節仍須以受控的實體裝置測試。

命令成功後為何沒有出現通知橫幅?

先確認通知權限、aps 內容與應用程式狀態。應用程式位於前景時不會必然顯示橫幅,必須由通知委派明確要求呈現。

CI 應使用 booted 還是固定的模擬器 UDID?

應使用工作所分配的明確 UDID。平行執行時,booted 可能選到其他模擬器,使通知、截圖與日誌混入不同工作。

獨享實體 Mac mini

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

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

選擇配置並租用