内购状态机最难复现的通常不是“购买成功”,而是取消、待处理、退款以及上一次交易未收尾。把这些分支直接交给外部交易环境,会让网络、商品配置和测试账户状态混入结果。更稳妥的做法,是先在云端 Mac 上用 StoreKit 配置文件固定商品,再由 SKTestSession 控制交易条件,把客户端逻辑变成可重复执行的回归测试。
先划清离线测试的边界
StoreKit 配置适合验证商品标识映射、购买状态转换、权益刷新、错误提示和交易完成逻辑。它不需要访问外部支付链路,失败时也更容易复现。
但它不能证明线上商品已经正确配置,也不能覆盖服务端通知、真实收据校验和最终付款界面。建议把测试拆成三层:
| 层级 | 主要目标 | 执行频率 |
|---|---|---|
| 单元测试 | 权益计算、状态映射 | 每次提交 |
| StoreKit 集成测试 | 购买、取消、退款、未完成交易 | 每次合并 |
| 外部环境验收 | 商品配置、通知与收据链路 | 发布前 |
离线测试的价值是缩短反馈回路,不是把所有交易风险伪装成一个绿色勾号。
固定商品与测试入口
在 Xcode 中创建 StoreKit Configuration File,只保留测试需要的商品。商品标识应与代码常量一致,价格仅用于界面测试,不要让业务逻辑根据本地化价格字符串判断权益。
把配置文件加入测试 Scheme,并建立独立测试计划,例如 StoreKitRegression.xctestplan。不要复用开发者日常运行的 Scheme,否则某次手工修改可能悄悄改变 CI 条件。
测试目标可在 setUpWithError() 中重建会话:
import StoreKitTest
import XCTest
final class PurchaseRegressionTests: XCTestCase {
private var session: SKTestSession!
override func setUpWithError() throws {
session = try SKTestSession(configurationFileNamed: "Products")
session.disableDialogs = true
session.clearTransactions()
session.failTransactionsEnabled = false
}
override func tearDownWithError() throws {
session.clearTransactions()
session = nil
}
}
配置文件名不带扩展名。若初始化失败,先确认文件属于测试目标,并检查 Scheme 是否使用同一份配置。
把交易分支拆成独立案例
不要在一个长用例中连续测试成功、退款和取消。状态越多,失败后越难判断污染来自哪一步。至少拆出以下案例:
正常购买与恢复
验证购买后权益立即刷新,重新创建业务层对象后仍能读取有效权益。测试结束前确认应用已经处理并完成交易,避免下一条用例再次收到旧更新。
取消与失败注入
取消不应显示“支付失败”,也不应写入已解锁状态。网络或通用交易错误则应保留重试入口。使用会话属性注入错误后,要在当前测试结束前恢复默认值,不能依赖下一条用例清理。
退款与未完成交易
退款后应撤销对应权益,但不要误删其他仍有效的商品。对未完成交易,要验证应用重启后仍能继续处理,而不是只检查一次按钮回调。
涉及同一商品状态的测试建议串行执行。并行测试若共享模拟器和配置,很容易互相清理交易,形成只在 CI 出现的偶发失败。
用命令行固定运行条件
先查看可用模拟器,再把设备名称、系统版本和 Xcode 路径写入 CI 变量。不要让 xcodebuild 自动选择任意目标。
set -euo pipefail
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
RESULT_DIR="$PWD/TestResults"
rm -rf "$RESULT_DIR"
mkdir -p "$RESULT_DIR"
xcodebuild test \
-workspace App.xcworkspace \
-scheme App-StoreKitTests \
-testPlan StoreKitRegression \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-resultBundlePath "$RESULT_DIR/StoreKit.xcresult"
OakVM 上若同时运行多条流水线,应为每个任务分配独立工作目录和模拟器,不要共用 DerivedData、结果目录或已启动设备。开始前可执行 xcrun simctl shutdown all,但它会影响同一节点上的其他任务,因此只适合节点由单个任务独占的执行方式。
归档证据并设置失败门槛
测试失败后,仅保存控制台末尾几十行通常不够。应归档 xcresult、提交哈希、StoreKit 配置文件校验值、Xcode 版本和模拟器运行时版本。配置文件发生变化时,也要进入代码审查,避免商品标识被无意删除。
可以先记录环境摘要:
xcodebuild -version
xcrun simctl list runtimes
shasum -a 256 Tests/StoreKit/Products.storekit
检查清单应包括:
- 每个测试是否创建并清理自己的
SKTestSession - 是否关闭自动弹窗,避免无人值守任务阻塞
- 取消、失败、退款是否分别断言界面状态与权益状态
- 测试是否依赖执行顺序或上一条交易
xcresult是否在失败后仍会上传- 日志是否避开敏感凭据与完整交易数据
当这些条件固定后,内购回归就不再依赖手工点击。外部环境只承担它真正需要验证的部分,日常提交则由快速、隔离且可追溯的测试守住客户端状态机。
常见问题
StoreKit 离线测试能否完全替代真实交易环境验收?
不能。它适合验证客户端状态机、商品映射和异常分支,但真实商品配置、服务端通知、收据链路与支付界面仍需在对应测试环境中单独验收。
为什么内购测试在 CI 中单独运行时通过,整套测试却偶发失败?
优先检查交易是否在用例间残留。每个测试应新建 SKTestSession,清空交易、关闭对话框,并让测试目标串行执行涉及同一商品状态的用例。
内购回归失败时最少应归档哪些证据?
至少保留 xcresult、测试日志、所用 StoreKit 配置文件版本、提交哈希、Xcode 版本和模拟器运行时版本,且不要在日志中记录真实凭据或完整交易数据。
在独享物理节点上运行下一次构建
选择 OakVM M4 或 OakVM M4 Pro,按天、周、月或季租用非虚拟机的云端 Mac。节点实际可用状态以控制台实时返回为准。