工程卷宗

用 StoreKit 配置建立可重复的 iOS 内购回归测试

用 StoreKit 配置建立可重复的 iOS 内购回归测试

内购状态机最难复现的通常不是“购买成功”,而是取消、待处理、退款以及上一次交易未收尾。把这些分支直接交给外部交易环境,会让网络、商品配置和测试账户状态混入结果。更稳妥的做法,是先在云端 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 BUILD NODE

在独享物理节点上运行下一次构建

选择 OakVM M4 或 OakVM M4 Pro,按天、周、月或季租用非虚拟机的云端 Mac。节点实际可用状态以控制台实时返回为准。

选择配置并租用