内购模块最难复现的故障,通常不是“按钮点不动”,而是交易状态跨用例泄漏:第一个测试完成购买,第二个测试本应验证未购买界面,却直接读到了已解锁状态。在云端 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 返回状态、权益仓库状态、最终界面状态。只断言按钮文字变化,会漏掉后台权益没有更新的问题。
提交前可按以下顺序检查:
.storekit文件与共享 Scheme 已进入版本控制。- 每个用例开始前清除交易和本地权益缓存。
- 交易监听器早于购买动作启动。
- 测试不依赖固定等待或执行顺序。
- 每个并行任务使用独立模拟器与产物目录。
- 失败时保留
xcresult、测试日志和界面附件。 - 正式发布前另行核对服务端通知与商品配置。
在 HireVM 上执行这类任务时,先在控制台确认当前可选配置,再按测试分片数量规划模拟器。内购测试通常更受状态隔离影响,而不是单次编译速度影响;先保证一条串行流水线可重复通过,再增加并发,排障成本会低得多。
常见问题
StoreKit Testing 能否完全替代生产环境中的内购验收?
不能。它适合验证商品映射、交易状态机、界面更新和异常分支;正式发布前仍应在受控的预发布流程中核对服务端通知、商品配置与真实交易链路。
为什么内购测试单独运行正常,进入完整测试套件后却失败?
常见原因是前一个用例遗留交易、续期状态或本地持久化数据。每个用例开始前应清理测试会话与应用状态,并避免多个测试同时操作同一模拟器。
云端 Mac 上应该并行运行多少组 StoreKit 测试?
先按一台模拟器对应一个串行测试分片配置。需要并行时,为每个分片分配独立模拟器和结果目录,不要让多个进程共享同一个 StoreKit 会话。
把下一项远程 Mac 任务放到独享节点运行
按任务规模选择两档在售配置、五个节点与日、周、月、季周期。下单前可完整核对配置和 USD 金额。