在云端 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 配置覆盖新加坡、东京、首尔与香港节点。实际可用状态以下单时返回的结果为准。

选择机型并订购