iOS 流水線可能在編譯階段全部通過,直到安裝至模擬器時才發現 App 套件結構異常,或是在啟動後立即退出。若一開始就執行完整的 UI 測試,不僅回饋緩慢,也會讓基礎故障看起來像測試逾時。更穩妥的做法,是在雲端 Mac 上加入一道能在兩三分鐘內完成的安裝與啟動冒煙檢查:固定執行階段、安裝剛產生的 .app、確認目標程序持續存活,最後保存啟動記錄。
冒煙檢查要回答哪些問題
這道檢查不驗證業務功能,只回答四個是非問題:模擬器能否正常啟動、產物能否安裝、指定的 Bundle ID 能否啟動,以及程序是否在觀察期間持續執行。任何一項失敗,都不應繼續占用後續 UI 測試的資源。
| 檢查點 | 成功條件 | 失敗時保留 |
|---|---|---|
| 啟動 | bootstatus 正常結束 |
裝置 UDID、執行階段版本 |
| 安裝 | simctl install 傳回 0 |
App 路徑、命令輸出 |
| 啟動 App | simctl launch 傳回 PID |
Bundle ID、結束碼 |
| 存活 | 觀察期結束後仍可查詢程序 | 系統記錄、當機記錄 |
冒煙檢查的價值不在於提高涵蓋率,而是將「產物存在」與「產物可以執行」拆成兩個明確結論。
建議將模擬器 UDID 明確傳入工作,不要依賴 booted。共用的雲端 Mac 上可能留有其他工作啟動的裝置;使用模糊目標,會使記錄與安裝結果互相混雜。NowMini 節點實際可選的設定應以控制台為準;流水線本身只需依賴已安裝的 Xcode 與對應的模擬器執行階段。
先固定建置產物與目標裝置
先將建置輸出集中到工作目錄,避免從 DerivedData 猜測路徑。Scheme、設定、Bundle ID 與 UDID 都應透過環境變數提供,並在工作開始時完成檢查。
set -euo pipefail
: "${SIM_UDID:?SIM_UDID is required}"
: "${APP_BUNDLE_ID:?APP_BUNDLE_ID is required}"
WORK_DIR="${RUNNER_TEMP:-$PWD/.smoke}"
APP_PATH="$WORK_DIR/build/Sample.app"
LOG_DIR="$WORK_DIR/logs"
rm -rf "$WORK_DIR"
mkdir -p "$LOG_DIR"
xcodebuild \
-workspace Sample.xcworkspace \
-scheme Sample \
-configuration Debug \
-sdk iphonesimulator \
-destination "platform=iOS Simulator,id=$SIM_UDID" \
CONFIGURATION_BUILD_DIR="$WORK_DIR/build" \
build
不要用 find 取得第一個 .app。專案可能同時產生宿主 App、測試宿主與擴充功能,而且順序並不固定。產品名稱已知時,應直接組出路徑,並在安裝前驗證 Info.plist:
test -d "$APP_PATH"
BUILT_ID=$(/usr/libexec/PlistBuddy -c \
"Print :CFBundleIdentifier" "$APP_PATH/Info.plist")
test "$BUILT_ID" = "$APP_BUNDLE_ID"
這一步可以提早發現 Scheme 指定錯誤、建置設定替換不正確,或安裝命令取得測試宿主等問題。
啟動、安裝並驗證程序
啟動裝置時應使用 bootstatus -b,等待系統真正就緒。若只執行 boot 就立刻安裝,在負載較高時,容易因服務尚未準備完成而間歇性失敗。
xcrun simctl boot "$SIM_UDID" 2>/dev/null || true
xcrun simctl bootstatus "$SIM_UDID" -b
xcrun simctl uninstall "$SIM_UDID" "$APP_BUNDLE_ID" \
2>/dev/null || true
xcrun simctl install "$SIM_UDID" "$APP_PATH"
LAUNCH_OUTPUT=$(xcrun simctl launch \
"$SIM_UDID" "$APP_BUNDLE_ID" \
-SmokeTestMode YES)
printf '%s
' "$LAUNCH_OUTPUT"
PID=$(printf '%s
' "$LAUNCH_OUTPUT" | awk -F': ' 'NF == 2 {print $2}')
test -n "$PID"
啟動參數可讓 App 略過首次使用引導、關閉動畫,或改用本機測試資料,但這些行為必須由應用程式程式碼明確實作,不能假設任意參數都會生效。啟動後先等待一段短暫的觀察期,再從模擬器內部查詢程序。若只檢查 launch 的傳回碼,會遺漏 App 在啟動後一秒內退出的情況。
sleep 8
xcrun simctl spawn "$SIM_UDID" launchctl print \
"gui/$(id -u)/$APP_BUNDLE_ID" >/dev/null
部分應用程式的程序標籤與 Bundle ID 不完全一致。此時應改為查詢已知的程序名稱,或讓應用程式在啟動時寫入一個僅供測試讀取的就緒標記。不要只靠增加固定等待時間來掩蓋不穩定狀態。
為失敗保存可讀的證據
清理動作很重要,但必須先保存證據。最小證據集包括建置輸出、裝置描述、安裝與啟動輸出,以及目標程序附近的統一記錄。記錄範圍應圍繞目前工作,而不是匯出整台主機的歷史記錄。
xcrun simctl list devices -j > "$LOG_DIR/devices.json"
xcrun simctl spawn "$SIM_UDID" log show \
--style compact \
--last 2m \
--predicate "process == 'Sample'" \
> "$LOG_DIR/sample-launch.log" 2>&1 || true
常見誤區是只封存標準錯誤。App 啟動後立即退出時,真正原因可能出現在模擬器的系統記錄中,例如動態程式庫載入失敗、資源路徑錯誤或未處理的例外狀況。如果記錄包含權杖、請求標頭或使用者資料,應在封存前去識別化,並限制流水線產物的存取範圍。
區分環境故障與應用程式故障
bootstatus 失敗通常屬於裝置環境問題;install 失敗時,應優先檢查套件結構、執行階段相容性與磁碟空間;若 launch 成功但程序消失,則應優先查看應用程式記錄。先完成故障分類,再決定是否重試。應用程式若以相同方式連續失敗,不應自動重跑多次,否則只會延遲真正有用的回饋。
清理狀態並接入流水線
無論工作成功或失敗,都應終止應用程式,並關閉這次使用的模擬器。可以使用 shell 的 trap,確保收尾動作一定執行:
cleanup() {
xcrun simctl terminate "$SIM_UDID" "$APP_BUNDLE_ID" \
2>/dev/null || true
xcrun simctl uninstall "$SIM_UDID" "$APP_BUNDLE_ID" \
2>/dev/null || true
xcrun simctl shutdown "$SIM_UDID" \
2>/dev/null || true
}
trap cleanup EXIT
如果工作要求每次都從完全相同的初始狀態開始,可以在關閉後執行 simctl erase;但這會增加執行時間,也會刪除預先放入的資料。更常見的折衷方式,是解除安裝 App、清理工作目錄,並為每條流水線分配獨立裝置。涉及權限狀態時,再明確重設目標服務。
最後,將這道檢查放在編譯之後、單元測試或 UI 測試之前。腳本應傳回明確的非零結束碼,記錄封存步驟則設定為一律執行。如此一來,安裝失敗不會偽裝成測試逾時,啟動即退也不會浪費整組執行器;每次失敗都能留下足夠證據,讓開發者使用相同的執行階段與同一份產物重現問題。
常見問題
為什麼不能只確認 xcodebuild 成功?
建置成功只代表產物已生成,無法證明模擬器能安裝並啟動它。套件內容、最低系統版本或執行期資源錯誤仍可能在後續階段出現。
冒煙檢查失敗時應保留哪些證據?
至少保留 xcodebuild 輸出、模擬器 UDID 與執行期版本、App 路徑、simctl install 和 launch 的結束碼,以及失敗前後的目標程序記錄。
在獨享實體節點上執行下一項任務
使用 M4、16GB RAM 與 256GB SSD 的雲端 Mac,依任務週期選擇新加坡、東京、首爾或香港節點。