在雲端 Mac 建立 iOS 資料庫遷移回歸關卡

在雲端 Mac 建立 iOS 資料庫遷移回歸關卡

一次看似普通的 Core Data 欄位調整,在全新安裝的模擬器中通常不會出現異常,卻可能讓直接從舊版本升級的使用者卡在啟動畫面。真正需要納入 CI 的,不是「空白資料庫能否建立」,而是舊資料庫能否沿著支援的路徑完成遷移、關鍵記錄是否維持一致,以及失敗時能否留下足夠的證據。雲端 Mac 可持續在線執行工作,適合將這類耗時但結果明確的檢查設為獨立關卡。

先定義遷移支援範圍

先列出目前仍可能存在的資料庫版本,不要假設使用者一定會逐版升級。若目前模型為 V5,而線上仍有 V3、V4 用戶端,就至少要測試 V3→V5 與 V4→V5。V5 空白資料庫測試只能證明目前模型可以載入,無法涵蓋舊欄位重新命名、關聯約束變更或唯一索引衝突。

建議維護一份簡單矩陣:

樣本 升級目標 必查內容
V3 基礎資料庫 V5 帳戶、專案、歷史任務數量
V4 邊界資料庫 V5 空關聯、重複名稱、已刪除標記
V4 大型樣本 V5 遷移完成時間與檔案大小峰值
V5 空白資料庫 V5 目前模型初始化

遷移關卡的通過條件應是「資料語意仍然成立」,而不只是持久化容器沒有拋出錯誤。

每個樣本還應記錄產生它的應用程式版本、模型識別碼、預期記錄數與驗證摘要。不要臨時從開發者日常使用的資料庫取樣,否則樣本會隨操作改變,發生失敗時也難以重現。

凍結可重複使用的舊資料庫樣本

為每個歷史版本準備專用建置版本,並讓它寫入固定資料:正常記錄、空值、長文字、已封存物件,以及接近約束邊界的物件都應包含在內。寫入完成後,先正常關閉持久化容器,再複製資料庫。

SQLite 使用 WAL 時,資料可能分散在三個檔案中。最穩妥的做法是讓應用程式完成儲存並執行 checkpoint;若無法保證,就將整組檔案一起封存:

set -euo pipefail

STORE="$HOME/Library/Developer/CoreSimulator/Devices/$SIM_UDID/data/Containers/Data/Application/$APP_ID/Library/Application Support/App.sqlite"
OUT="MigrationFixtures/V4"

mkdir -p "$OUT"
cp "$STORE" "$OUT/App.sqlite"

for suffix in -wal -shm; do
  if [ -f "${STORE}${suffix}" ]; then
    cp "${STORE}${suffix}" "$OUT/App.sqlite${suffix}"
  fi
done

find "$OUT" -type f -print0 | sort -z | xargs -0 shasum -a 256 > "$OUT/SHA256SUMS"

不要在測試中直接開啟儲存庫內的原始樣本。每項測試都應先將樣本複製到暫存目錄,遷移完成後再刪除,確保平行執行的任務不會互相覆寫。若樣本含有真實使用者內容、權杖或連線資訊,應重新製作匿名資料,而不是依賴後續去識別化處理。

將遷移封裝成可測試的進入點

應用程式的啟動流程經常將資料庫載入、介面初始化與網路請求綁在一起,導致遷移失敗時難以定位問題。應將持久化容器的建立流程封裝成可注入 URL 的元件,讓 XCTest 能直接載入暫存副本。

func makeContainer(storeURL: URL) throws -> NSPersistentContainer {
    let container = NSPersistentContainer(name: "AppModel")
    let description = NSPersistentStoreDescription(url: storeURL)
    description.shouldMigrateStoreAutomatically = true
    description.shouldInferMappingModelAutomatically = true
    description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
    container.persistentStoreDescriptions = [description]

    var loadError: Error?
    container.loadPersistentStores { _, error in
        loadError = error
    }
    if let loadError {
        throw loadError
    }
    return container
}

輕量級遷移適合新增選用欄位、加入具有預設值的屬性,以及處理明確的重新命名。若涉及實體拆分、資料合併或值轉換,則應提供明確的對應模型或採用分階段遷移。不要為了讓測試通過而刪除舊模型;歷史模型是讀取舊資料庫中繼資料及推導遷移路徑時不可或缺的輸入。

驗證業務不變量

遷移後至少要檢查四個層面:持久化儲存區是否成功載入、模型版本是否已更新、記錄數是否符合預期,以及關鍵關聯與欄位語意是否正確。例如,任務總數不得改變,已封存的專案仍應維持封存狀態,孤立子物件的數量應為零。

比對時不要只依賴自動產生的物件 ID。可以為測試資料寫入穩定的業務鍵,再依業務鍵檢查欄位。對於排序或集合關聯,應先正規化再進行比較,避免無意義的順序差異造成偶發失敗。

在雲端 Mac 接入獨立測試任務

遷移測試應與一般單元測試分開執行,因為它需要複製樣本,並頻繁建立持久化儲存區。固定模擬器執行階段與目標裝置名稱,先啟動裝置,再執行指定的測試計畫:

set -euo pipefail

xcrun simctl bootstatus "$SIM_UDID" -b

xcodebuild test \
  -workspace App.xcworkspace \
  -scheme App \
  -testPlan DatabaseMigration \
  -destination "platform=iOS Simulator,id=$SIM_UDID" \
  -resultBundlePath Artifacts/MigrationTests.xcresult \
  CODE_SIGNING_ALLOWED=NO

測試任務開始前先清理暫存目錄;結束後無論成功或失敗,都要封存 xcresult、遷移日誌、樣本摘要與目標模型識別碼。不要將遷移後的完整資料庫設為預設成品上傳;其中可能含有測試用祕密資訊,也會使儲存空間用量迅速增加。通常只在失敗時保留匿名樣本副本,並設定明確的清理週期。

在 ZoomMini 上執行時,將任務綁定至固定的 Xcode 工具鏈,並在控制台確認目前可選的配置。更換 Xcode 或模擬器執行階段後,先單獨執行遷移計畫,再恢復完整流水線,避免工具鏈與模型同時發生變更,增加問題歸因的難度。

常見失敗與發布檢查項目

「本機通過、CI 失敗」通常是因為檔案組複製不完整、樣本遭到上一次測試修改、模型資源未包含在測試套件內,或多項測試同時操作相同路徑。先核對 SHA-256,再檢查暫存目錄是否唯一,最後確認編譯產物中包含所有受支援的模型版本。

發布前逐項確認:

  • 每個受支援的起始版本都有固定樣本與預期摘要;
  • SQLite 主檔案與 WAL 狀態一致;
  • 測試只遷移暫存副本,不會修改原始 fixture;
  • 輕量級遷移與明確對應模型的適用邊界已清楚記錄;
  • 記錄數、穩定業務鍵、關聯與預設值均有斷言;
  • 失敗的任務會保留 xcresult 與去識別化的遷移日誌;
  • 刪除模型版本、重新命名欄位及變更約束時,必須觸發遷移計畫;
  • 完整測試會從最舊的受支援版本直接升級至目前版本。

資料庫遷移不是一次性的發布指令碼,而是一份會隨模型演進持續擴充的相容性契約。將歷史樣本、業務不變量與失敗證據固定下來後,每次模型調整都能在合併前回答同一個問題:現有資料是否仍能安全抵達新版本。

常見問題

資料庫遷移測試至少要保留哪些歷史版本?

至少保留目前線上版本與前一個仍可能直接升級的版本;若產品支援較長的升級跨度,就要覆蓋所有仍受支援的起始版本。

為什麼不能只複製 SQLite 主檔案?

啟用 WAL 後,已提交但尚未合併的資料可能位於 -wal 檔案。除非先安全執行 checkpoint,否則應完整保存 sqlite、sqlite-wal 與 sqlite-shm。

ZoomMini 雲端 Mac

選擇獨享實體節點,滿足建置、測試與實驗需求

兩檔 M4 配置涵蓋新加坡、東京、首爾與香港節點。實際可用狀態以下單時回傳的結果為準。

選擇機型並訂購