使用者在雲端 Mac 上編輯一份尚未送出的表單,切換到其他應用程式處理資料後,程序可能在幾分鐘內被系統終止。再次開啟應用程式時,最糟糕的情況並不是當機,而是介面看似正常,草稿卻已遭清空、導覽退回首頁,或還原出已經失效的物件。這類問題很難透過一般單元測試涵蓋,更適合在獨立模擬器中使用 XCUITest 重現完整生命週期。
先定義哪些狀態應該還原
不要把「還原上次的畫面」視為單一需求。開始撰寫測試前,先將狀態分成三類:
| 狀態類型 | 範例 | 重新啟動後的預期結果 |
|---|---|---|
| 業務狀態 | 草稿文字、篩選條件、選取項目 | 依產品規則還原 |
| 導覽狀態 | 目前頁面、分頁、清單位置 | 還原至仍然有效的層級 |
| 暫時狀態 | 載入動畫、彈出視窗、一次性權杖 | 捨棄並重新計算 |
還原邏輯也必須區分正常切換至背景與異常終止。前者可以保留更多介面細節,後者則應優先確保資料一致性。如果草稿依賴的物件已被刪除,應用程式應返回安全頁面並說明原因,而不是強行重建已失效的介面。
狀態還原測試的目標並不是讓每個像素都回到原位,而是確保使用者能夠繼續工作,且不會看到過期、無權存取或無法送出的資料。
建議為每個情境列出「準備狀態、終止動作、還原斷言、禁止出現的狀態」四個項目。如此一來,測試失敗時,團隊便能判斷問題出在持久化、路由還是資料驗證。
為自動化準備確定性進入點
UI 測試不應依賴前一次執行留下的資料。應用程式可在收到僅供測試使用的啟動參數時匯入固定測試資料,並將資料寫入獨立容器。測試資料名稱應表達業務情境,而不是使用容易變動的資料庫主鍵。
let arguments = ProcessInfo.processInfo.arguments
if arguments.contains("-ui-testing"),
let index = arguments.firstIndex(of: "-fixture"),
arguments.indices.contains(index + 1) {
let fixture = arguments[index + 1]
try TestFixtureLoader.load(named: fixture)
}
正式版建置必須忽略或移除這個測試進入點。更穩妥的做法是使用編譯條件,將載入器限制在內部測試設定中。測試開始前還應清除通知、快取與共享偏好設定,避免因為「舊草稿恰好存在」而造成誤判通過。
即使在 Runner 的獨享實體節點上執行,也應為每條流水線配置獨立的模擬器裝置集。獨享運算資源不代表測試資料會自然隔離;兩個並行工作共用同一個模擬器時,仍會彼此覆寫應用程式容器。
使用 XCUITest 重現程序終止與重新啟動
以下測試會先建立草稿,再將應用程式送入背景、終止程序並重新啟動。所有關鍵控制項都應設定穩定的 accessibility identifier,避免依賴本地化文字尋找元素。
func testDraftRestoresAfterTermination() {
let app = XCUIApplication()
app.launchArguments = [
"-ui-testing",
"-fixture", "empty-project",
"-reset-state", "YES"
]
app.launch()
app.buttons["project.create"].tap()
let editor = app.textViews["draft.editor"]
editor.tap()
editor.typeText("release checklist")
app.buttons["draft.save"].tap()
XCUIDevice.shared.press(.home)
app.terminate()
app.launchArguments = [
"-ui-testing",
"-fixture", "empty-project"
]
app.launch()
XCTAssertTrue(app.navigationBars["draft.screen"].waitForExistence(timeout: 8))
XCTAssertEqual(app.textViews["draft.editor"].value as? String,
"release checklist")
XCTAssertTrue(app.staticTexts["restoration.completed"].exists)
}
不要在同一項測試的第二次啟動中繼續傳入重設參數,否則測試會自行刪除需要驗證的狀態。也不要只斷言編輯器存在;還應同時檢查內容、導覽標題、選取物件與還原提示。
分別測試三種啟動路徑
至少保留三項獨立測試案例:
- 從背景直接回到前景,驗證短暫離開不會重建整個頁面。
- 儲存狀態後終止程序,驗證重新啟動後仍能繼續工作。
- 建立失效物件後重新啟動,驗證應用程式會安全降級並清除無效路由。
每項案例只驗證一種生命週期,失敗時會比包含十多個步驟的長測試更容易直接定位問題。
在雲端 Mac 上隔離模擬器與執行目錄
自動化工作應明確建立暫時裝置集,而不是使用登入使用者預設目錄中的模擬器。工作結束後刪除裝置集,即可一併清除應用程式容器與殘留快照。
set -euo pipefail
DEVICE_SET="$RUNNER_TEMP/CoreSimulator"
RESULTS="$RUNNER_TEMP/StateRestoration.xcresult"
mkdir -p "$DEVICE_SET"
xcrun simctl --set "$DEVICE_SET" create \
"State-Restore-iPhone" \
"com.apple.CoreSimulator.SimDeviceType.iPhone-16" \
"com.apple.CoreSimulator.SimRuntime.iOS-18-0"
UDID="$(xcrun simctl --set "$DEVICE_SET" list devices \
available -j | /usr/bin/python3 scripts/first_device.py)"
xcrun simctl --set "$DEVICE_SET" boot "$UDID"
xcodebuild test \
-workspace Example.xcworkspace \
-scheme ExampleUITests \
-destination "platform=iOS Simulator,id=$UDID" \
-resultBundlePath "$RESULTS"
xcrun simctl --set "$DEVICE_SET" shutdown "$UDID" || true
rm -rf "$DEVICE_SET"
範例中的執行階段與裝置類型必須替換為節點上已安裝的版本,可先執行 xcrun simctl list runtimes 與 xcrun simctl list devicetypes 進行確認。不要讓指令碼靜默選擇「最新」執行階段,否則 Xcode 更新後,螢幕截圖、系統彈出視窗與還原行為都可能一起改變。
將失敗證據轉化為可診斷的結果
狀態還原失敗通常發生在第二次啟動後,因此需要保留還原前後的證據。建議在關鍵動作後附加螢幕截圖,並將測試使用的固定資料名稱、模擬器識別碼、應用程式版本與還原階段寫入 XCTest attachment。測試失敗時保留 .xcresult,成功工作的保存時間則可依團隊政策縮短。
疑難排解順序可固定如下:
- 確認第一次啟動確實已寫入狀態。
- 檢查進入背景時是否完成不可分割的儲存作業。
- 確認第二次啟動沒有再次執行清理邏輯。
- 檢查持久化資料的版本遷移是否成功。
- 最後檢查路由是否接受還原後的物件。
如果草稿已還原,但頁面回到首頁,問題多半出在導覽重建;如果頁面正確但欄位為空,則應檢查持久化時機與儲存鍵;如果只有並行工作失敗,應優先確認模擬器、結果目錄與固定測試資料是否真正隔離。
將這些情境納入合併門檻後,狀態還原便不再依賴人工反覆切換應用程式。每次修改持久化模型、Scene 生命週期或導覽結構時,流水線都能使用相同輸入,驗證使用者是否仍可從中斷處繼續工作。
常見問題
狀態還原測試可以直接使用開發者日常操作的模擬器嗎?
不建議。自動化工作應使用獨立模擬器或裝置集,並在每個案例開始前清除 App 資料,避免既有狀態造成錯誤通過。
為什麼不能只檢查 App 是否成功重新開啟?
成功開啟只能證明沒有立即崩潰,還要驗證草稿內容、導覽層級、選取項目與還原提示,並確認敏感暫存資料沒有被不當保留。
在雲端 M4 Mac 上執行下一項工作
選擇 Runner M4 或 Runner M4 Plus,依專案需求決定節點、租期與儲存空間附加選項。每筆訂單皆對應一台獨享實體機,並非虛擬機。