HireVM 工程记录

云端 Mac 上用 StoreKit Testing 建立可重复的内购回归测试

云端 Mac 上用 StoreKit Testing 建立可重复的内购回归测试

内购模块最难复现的故障,通常不是“按钮点不动”,而是交易状态跨用例泄漏:第一个测试完成购买,第二个测试本应验证未购买界面,却直接读到了已解锁状态。在云端 Mac 上长期跑回归测试时,模拟器、StoreKit 会话和应用持久化数据必须一起隔离,否则单测通过、整套失败会成为常态。

先把测试边界分成三层

第一层是纯业务状态机。输入“未购买、处理中、已购买、已退款、已过期”,检查功能权限与界面状态,不启动真实交易。第二层使用 StoreKit Testing,验证商品查询、购买回调、交易监听和订阅变化。第三层才是发布前的受控验收,用于核对服务端通知与正式商品配置。

这样的拆分能让大多数回归在本地完成,也能明确失败属于业务逻辑、客户端交易适配,还是外部配置。不要让每个界面测试都从点击购买按钮开始;状态机单测应占多数,StoreKit 集成测试只覆盖关键路径。

StoreKit Testing 提供的是可控制的交易环境,不是生产交易链路的替身。测试通过表示客户端在给定事件下行为正确,不代表外部配置已经完成验收。

建立可审查的商品目录

在工程中创建 StoreKit/Local.storekit,商品标识必须与代码使用的标识一致。建议把标识集中定义,避免散落在视图和测试中:

enum ProductID {
    static let proMonthly = "com.example.app.pro.monthly"
    static let proYearly = "com.example.app.pro.yearly"
}

配置文件应纳入版本控制,但测试价格、显示名称和订阅周期的修改也要经过代码审查。团队常见的坑是复制旧配置后只改显示名称,没有改商品标识,导致商品查询返回空数组。

为测试建立独立 Scheme,例如 StoreKitRegression,在 Scheme 的 Run 与 Test 选项中选择同一份 .storekit 文件。不要依赖开发者个人 Scheme;共享 Scheme 才能被 xcodebuild 找到。

检查项 预期结果
商品标识 配置文件、代码和断言完全一致
Scheme 标记为 Shared 并提交到仓库
订阅组 同组升级、降级关系清晰
本地化 测试断言不依赖易变的展示文案

每个用例都从干净会话开始

使用 StoreKitTest 创建测试会话,并在 setUp 中关闭系统对话框、清除交易。测试方法结束后,还要清理应用自己保存的权益缓存。

import XCTest
import StoreKitTest

final class PurchaseRegressionTests: XCTestCase {
    private var session: SKTestSession!

    override func setUpWithError() throws {
        session = try SKTestSession(
            configurationFileNamed: "Local.storekit"
        )
        session.disableDialogs = true
        session.clearTransactions()
        UserDefaults.standard.removeObject(forKey: "cachedEntitlements")
    }

    func testMonthlyPurchaseUnlocksPro() async throws {
        try await session.buyProduct(
            identifier: ProductID.proMonthly
        )
        let unlocked = await EntitlementStore.shared.refresh()
        XCTAssertTrue(unlocked)
    }
}

交易监听器必须在发起购买前启动。否则购买完成得很快时,监听任务可能错过更新,测试会偶发超时。退款、撤销和订阅过期也应由会话主动触发,再断言权益刷新后的结果,而不是只检查购买 API 是否返回成功。

不要用固定等待掩盖竞态

Task.sleep 只能推迟失败,不能证明状态已更新。更稳妥的方式是让权益仓库暴露可观察状态,测试等待明确条件,并设置较短超时。超时日志至少记录商品标识、当前权益、未完成交易数量和测试名称,但不要输出令牌或完整凭据。

用命令行固定执行入口

先确认云端 Mac 已安装的模拟器设备,再让 CI 使用固定名称。若镜像中的设备名称不同,应调整流水线参数,而不是让脚本静默选择任意设备。

xcrun simctl list devices available
xcodebuild test \
  -workspace Example.xcworkspace \
  -scheme StoreKitRegression \
  -destination 'platform=iOS Simulator,name=iPhone 16 Pro' \
  -resultBundlePath Artifacts/StoreKitTests.xcresult \
  CODE_SIGNING_ALLOWED=NO

只运行模拟器测试时关闭签名,可减少与交易逻辑无关的失败。结果目录必须在运行前清空或使用任务编号分目录,否则已有的 xcresult 会让命令直接报错。

StoreKit 集成测试默认串行更稳。需要并行时,每个执行单元应拥有独立模拟器、Derived Data 和结果目录。多个进程同时操作同一台模拟器,会让交易队列和应用数据互相污染。

把失败设计成可定位的证据

至少覆盖首次购买、用户取消、待处理、退款、订阅续期和订阅过期六条路径。每条路径都应断言三个层面:StoreKit 返回状态、权益仓库状态、最终界面状态。只断言按钮文字变化,会漏掉后台权益没有更新的问题。

提交前可按以下顺序检查:

  1. .storekit 文件与共享 Scheme 已进入版本控制。
  2. 每个用例开始前清除交易和本地权益缓存。
  3. 交易监听器早于购买动作启动。
  4. 测试不依赖固定等待或执行顺序。
  5. 每个并行任务使用独立模拟器与产物目录。
  6. 失败时保留 xcresult、测试日志和界面附件。
  7. 正式发布前另行核对服务端通知与商品配置。

在 HireVM 上执行这类任务时,先在控制台确认当前可选配置,再按测试分片数量规划模拟器。内购测试通常更受状态隔离影响,而不是单次编译速度影响;先保证一条串行流水线可重复通过,再增加并发,排障成本会低得多。

常见问题

StoreKit Testing 能否完全替代生产环境中的内购验收?

不能。它适合验证商品映射、交易状态机、界面更新和异常分支;正式发布前仍应在受控的预发布流程中核对服务端通知、商品配置与真实交易链路。

为什么内购测试单独运行正常,进入完整测试套件后却失败?

常见原因是前一个用例遗留交易、续期状态或本地持久化数据。每个用例开始前应清理测试会话与应用状态,并避免多个测试同时操作同一模拟器。

云端 Mac 上应该并行运行多少组 StoreKit 测试?

先按一台模拟器对应一个串行测试分片配置。需要并行时,为每个分片分配独立模拟器和结果目录,不要让多个进程共享同一个 StoreKit 会话。

独享 Apple Silicon 物理节点

把下一项远程 Mac 任务放到独享节点运行

按任务规模选择两档在售配置、五个节点与日、周、月、季周期。下单前可完整核对配置和 USD 金额。

选择配置并下单