Test Store 概述
0:02:00欢迎来到 iOS 版 RevenueCat Test Store 设置指南!
测试应用内购买一直是一个具有挑战性的任务。您需要配置沙盒账户、创建测试产品、等待应用审核,还要处理不稳定的网络条件。RevenueCat 的 Test Store 通过提供确定性、快速且可靠的测试环境,消除了这些痛点。
什么是 Test Store?
Test Store 是 RevenueCat 内置的测试环境,允许您在不连接 App Store 的情况下测试应用内购买流程。它会自动为每个新的 RevenueCat 项目进行配置,让您完全控制购买结果。
您将学到什么
本 Codelab 将指导您完成为 iOS 开发设置和使用 RevenueCat Test Store 的完整过程。您将从在 RevenueCat 仪表板中启用 Test Store 并配置 iOS 应用程序使用 Test Store API 密钥开始。然后,您将学习如何使用确定性结果测试购买流程、为应用内购买逻辑编写自动化单元测试,以及使用 GitHub Actions 将这些测试集成到您的 CI/CD 流水线中。
Test Store 的优势
Test Store 彻底改变了您测试应用内购买的方式。与传统测试方法需要等待 App Store Connect 配置或应用审批不同,Test Store 提供即时测试功能。您可以完全确定性地控制购买结果:交易是成功、失败还是取消完全由您决定。这意味着不再需要处理不稳定的网络连接或不可靠的沙盒环境。
当您开始编写自动化测试时,真正的力量就显现出来了。使用 Test Store,您可以为购买逻辑构建可靠的单元测试,这些测试在任何环境中都能一致运行,包括像 GitHub Actions 这样的 CI/CD 系统。这使您能够快速迭代购买流程,在几秒钟内而不是几分钟或几小时内测试更改。
前提条件
在开始之前,请确保您具备:
- 一个 RevenueCat 账户(在 revenuecat.com 免费注册)
- 已集成 RevenueCat SDK(5.x 或更高版本)的 iOS 项目
- Swift 和 iOS 开发的基础知识
- 熟悉 SwiftUI(可选,用于 UI 示例)
在仪表板中启用 Test Store
0:03:00第一步是在 RevenueCat 仪表板中启用 Test Store 并获取您的 Test Store API 密钥。
访问仪表板
首先登录您的 RevenueCat 仪表板,然后导航到左侧边栏菜单中的 Apps & providers 部分。这里是您可以找到所有已连接的应用和可用商店集成的地方。
创建您的 Test Store
在 Apps & providers 部分,在可用的提供商中找到 Test Store 选项。Test Store 会自动为每个 RevenueCat 项目配置,因此您只需点击 Create Test Store 或 Enable Test Store 来激活它。设置是即时的,无需等待审批或配置同步。
获取您的 API 密钥
启用 Test Store 后,点击应用列表中的 Test Store 条目查看其详细信息。在这里您会找到您的 Test Store API 密钥,它以前缀 test_ 开头。这个密钥的功能与常规 RevenueCat API 密钥相同,但有一个关键区别:它将所有购买请求路由到 Test Store 而不是 App Store,让您完全控制测试环境。
理解 API 密钥分离: Test Store API 密钥与您的生产和沙盒密钥完全分开。这种分离是有意为之的,对安全性很重要。永远不要在生产构建中使用 Test Store 密钥;它们只应在调试和测试环境中使用。如果需要针对不同的测试场景,您可以为每个项目创建多个 Test Store 配置。
下一步
现在您已经有了 Test Store API 密钥,您可以准备配置 iOS 应用程序以使用 Test Store 进行测试了。
在 iOS 应用中配置 Test Store
0:05:00现在让我们配置您的 iOS 应用程序以使用 Test Store。关键是在运行测试时使用 Test Store API 密钥而不是生产 API 密钥。
步骤 1:通过 Xcode 配置添加 Test Store API 密钥
创建一个 xcconfig 文件或使用 Xcode 构建设置来按配置管理您的 API 密钥。创建一个名为 Debug.xcconfig 的文件:
// Debug.xcconfig
REVENUECAT_API_KEY = test_YOUR_KEY_HERE和一个 Release.xcconfig:
// Release.xcconfig
REVENUECAT_API_KEY = appl_YOUR_PRODUCTION_KEY然后将 REVENUECAT_API_KEY 添加到您的 Info.plist:
<key>RevenueCatAPIKey</key>
<string>$(REVENUECAT_API_KEY)</string>步骤 2:在应用中初始化 SDK
更新您的 @main App 结构体以使用适当的 API 密钥配置 RevenueCat:
import RevenueCat
import SwiftUI
@main
struct MyApp: App {
init() {
// 从 Info.plist 读取 API 密钥(通过 xcconfig 设置)
let apiKey = Bundle.main.infoDictionary?["RevenueCatAPIKey"] as? String
?? "test_YOUR_KEY_HERE"
Purchases.configure(withAPIKey: apiKey)
// 为测试构建启用调试日志
#if DEBUG
Purchases.logLevel = .debug
#endif
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}步骤 3:环境特定配置
对于更高级的设置,您可以使用 #if DEBUG 预处理器指令创建一个配置助手:
enum RevenueCatConfig {
static var apiKey: String {
#if DEBUG
return "test_YOUR_KEY_HERE"
#else
return "appl_YOUR_PRODUCTION_KEY"
#endif
}
static func configure() {
Purchases.configure(withAPIKey: apiKey)
#if DEBUG
Purchases.logLevel = .verbose
#endif
}
}然后在应用初始化中使用它:
@main
struct MyApp: App {
init() {
RevenueCatConfig.configure()
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}验证
要验证 Test Store 是否正常工作:
- 在 Xcode 中以调试模式运行您的应用
- 检查 Xcode 控制台中是否有
"Purchases SDK initialized with Test Store" - SDK 会指示它连接到 Test Store 而不是 App Store
交互式测试购买流程
0:05:00启用 Test Store 后,您现在可以完全控制购买流程的结果来进行测试。当 Test Store 显示其购买对话框时,您决定结果。
理解 Test Store 购买对话框
当您使用 Test Store 发起购买时,您会看到一个自定义的 Test Store 对话框,而不是标准的 App Store 购买界面。此对话框像真正的购买对话框一样显示产品详情和价格,但有一个关键区别:您可以控制结果。对话框提供三个明确的选项:购买成功模拟完成的交易,购买失败测试支付失败,以及取消模拟用户取消。这种确定性行为正是 Test Store 如此强大的原因。
测试成功流程
让我们测试一个成功的购买流程:
func purchaseProduct() async {
do {
// 从 Test Store 获取产品
let offerings = try await Purchases.shared.offerings()
guard let package = offerings.current?.availablePackages.first else {
return
}
// 发起购买
let (_, customerInfo, _) = try await Purchases.shared.purchase(package: package)
// 检查结果
let isPremium = customerInfo.entitlements["premium"]?.isActive == true
if isPremium {
// 购买成功!
showPremiumContent()
}
} catch {
// 处理错误
handlePurchaseError(error)
}
}测试正常路径:像平常一样运行您的应用并触发购买流程。当 Test Store 对话框出现时,点击"购买成功"。您的应用应该立即授予高级访问权限,您可以验证权益是否正确更新。这让您可以快速验证成功流程是否正常工作,无需真实的支付方式或沙盒账户。
测试失败场景
现在测试您的应用如何处理失败:
func handlePurchaseError(_ error: Error) {
guard let purchasesError = error as? RevenueCat.ErrorCode else {
showError("Unexpected error: \(error.localizedDescription)")
return
}
switch purchasesError {
case .purchaseCancelledError:
// 用户取消 - 不显示错误
print("User cancelled purchase")
case .purchaseInvalidError:
// 无效购买
showError("This purchase is not available")
case .paymentPendingError:
// 支付待处理(例如,等待家长批准)
showPendingMessage()
default:
// 其他错误
showError("Purchase failed: \(error.localizedDescription)")
}
}测试错误处理:再次触发购买流程,但这次当 Test Store 对话框出现时,点击"购买失败"。您的应用应该优雅地处理错误,向用户显示适当的错误消息。确认用户没有获得高级访问权限,并且应用的状态保持一致。这对于确保您的错误处理逻辑在生产环境中正确工作至关重要。
测试取消
struct PaywallView: View {
@State private var isPurchasing = false
var onDismiss: () -> Void
var body: some View {
Button(action: {
Task {
isPurchasing = true
do {
try await purchaseProduct()
} catch let error as RevenueCat.ErrorCode
where error == .purchaseCancelledError {
// 用户取消 - 只需关闭
onDismiss()
} catch {
// 处理其他错误
handlePurchaseError(error)
}
isPurchasing = false
}
}) {
Text(isPurchasing ? "Processing..." : "Subscribe")
}
.disabled(isPurchasing)
}
}测试用户取消:打开您的付费墙并点击订阅按钮。当 Test Store 对话框出现时,点击"取消"模拟用户退出购买。您的付费墙应该干净地关闭,不显示任何错误消息(取消是正常的用户操作,不是错误状态)。验证没有记录购买,并且您的应用状态保持不变。
关键要点
Test Store 的美妙之处在于其确定性控制。您决定每次购买尝试的确切结果。这意味着您可以在几分钟内彻底测试所有场景(成功、失败和取消),无需真实的支付方式,也不需要处理沙盒账户的延迟和复杂性。当您准备好与 App Store 集成时,您将对自己的购买处理逻辑非常有信心。
编写自动化单元测试
0:08:00Test Store 最强大的功能之一是支持您的应用内购买逻辑的自动化单元测试。让我们编写能够在 CI/CD 中可靠运行的测试。
步骤 1:设置 XCTest
XCTest 内置于 Xcode 中,因此无需外部依赖项。只需确保您的项目有一个测试目标。如果您使用了 Xcode 的默认项目模板,则已经包含了一个。您的 Package.swift 或 Xcode 项目应该已经将 RevenueCat 作为依赖项:
// 在您的 Package.swift 或通过 Xcode SPM
dependencies: [
.package(url: "https://github.com/RevenueCat/purchases-ios.git", from: "5.58.0")
]步骤 2:创建购买仓库协议
首先,让我们使用 Swift 协议创建一个可测试的购买仓库:
protocol PurchaseRepository {
func getOfferings() async throws -> Offerings
func purchase(package: Package) async throws -> CustomerInfo
func getCustomerInfo() async throws -> CustomerInfo
}
final class PurchaseRepositoryImpl: PurchaseRepository {
func getOfferings() async throws -> Offerings {
return try await Purchases.shared.offerings()
}
func purchase(package: Package) async throws -> CustomerInfo {
let (_, customerInfo, _) = try await Purchases.shared.purchase(package: package)
return customerInfo
}
func getCustomerInfo() async throws -> CustomerInfo {
return try await Purchases.shared.customerInfo()
}
}步骤 3:创建带有业务逻辑的 ViewModel
enum PaywallState {
case loading
case success(packages: [Package])
case purchasing
case purchaseSuccess
case purchaseCancelled
case error(message: String)
}
@MainActor
final class PaywallViewModel: ObservableObject {
@Published var state: PaywallState = .loading
private let repository: PurchaseRepository
init(repository: PurchaseRepository) {
self.repository = repository
}
func loadProducts() async {
do {
let offerings = try await repository.getOfferings()
let packages = offerings.current?.availablePackages ?? []
state = .success(packages: packages)
} catch {
state = .error(message: error.localizedDescription)
}
}
func purchase(package: Package) async {
state = .purchasing
do {
let customerInfo = try await repository.purchase(package: package)
let isPremium = customerInfo.entitlements["premium"]?.isActive == true
if isPremium {
state = .purchaseSuccess
} else {
state = .error(message: "Purchase completed but entitlement not active")
}
} catch let error as RevenueCat.ErrorCode
where error == .purchaseCancelledError {
state = .purchaseCancelled
} catch {
state = .error(message: error.localizedDescription)
}
}
}步骤 4:使用 XCTest 编写单元测试
现在使用模拟仓库创建单元测试:
import XCTest
@testable import MyApp
final class MockPurchaseRepository: PurchaseRepository {
var mockOfferings: Offerings?
var mockCustomerInfo: CustomerInfo?
var mockError: Error?
func getOfferings() async throws -> Offerings {
if let error = mockError { throw error }
return mockOfferings!
}
func purchase(package: Package) async throws -> CustomerInfo {
if let error = mockError { throw error }
return mockCustomerInfo!
}
func getCustomerInfo() async throws -> CustomerInfo {
if let error = mockError { throw error }
return mockCustomerInfo!
}
}
@MainActor
final class PaywallViewModelTests: XCTestCase {
private var mockRepository: MockPurchaseRepository!
private var viewModel: PaywallViewModel!
override func setUp() async throws {
mockRepository = MockPurchaseRepository()
viewModel = PaywallViewModel(repository: mockRepository)
}
func testPurchaseSuccess() async {
// Given: 模拟成功购买并激活权益
// 设置带有活跃 "premium" 权益的 mockCustomerInfo
// When: 发起购买
// await viewModel.purchase(package: mockPackage)
// Then: 状态应该为 purchaseSuccess
// XCTAssertEqual(viewModel.state, .purchaseSuccess)
}
func testPurchaseCancelled() async {
// Given: 模拟取消购买
mockRepository.mockError = RevenueCat.ErrorCode.purchaseCancelledError
// When: 发起购买
// await viewModel.purchase(package: mockPackage)
// Then: 状态应该为 purchaseCancelled
// XCTAssertEqual(viewModel.state, .purchaseCancelled)
}
func testPurchaseFailed() async {
// Given: 模拟失败购买
mockRepository.mockError = RevenueCat.ErrorCode.paymentPendingError
// When: 发起购买
// await viewModel.purchase(package: mockPackage)
// Then: 状态应该显示错误
// if case .error(let message) = viewModel.state {
// XCTAssertTrue(message.contains("pending"))
// } else {
// XCTFail("Expected error state")
// }
}
}步骤 5:基于 Mock 的测试替代方案
为了获得更多控制,您可以使用完全模拟的方法:
final class SpyPurchaseRepository: PurchaseRepository {
var getOfferingsCalled = false
var purchaseCalled = false
var lastPurchasedPackage: Package?
var stubbedOfferings: Offerings!
var stubbedCustomerInfo: CustomerInfo!
var stubbedError: Error?
func getOfferings() async throws -> Offerings {
getOfferingsCalled = true
if let error = stubbedError { throw error }
return stubbedOfferings
}
func purchase(package: Package) async throws -> CustomerInfo {
purchaseCalled = true
lastPurchasedPackage = package
if let error = stubbedError { throw error }
return stubbedCustomerInfo
}
func getCustomerInfo() async throws -> CustomerInfo {
if let error = stubbedError { throw error }
return stubbedCustomerInfo
}
}为什么这种方法有效
这种测试方法消除了测试应用内购买的传统痛点。因为 Test Store 没有网络依赖,您的测试每次都能可靠运行。不再有因连接问题导致的不稳定失败。测试在几秒钟而不是几分钟内执行,让您在开发过程中获得快速反馈。您可以在所有场景(包括难以用真实支付系统重现或耗时的边缘情况)中实现完整的测试覆盖。
我们构建的架构(使用协议模式将业务逻辑与 SDK 分离)使这些测试既快速又可维护。您可以在任何 CI/CD 环境(如 GitHub Actions)中运行它们,在 bug 到达生产环境之前捕获它们。
在 CI/CD 中运行测试
0:04:00现在让我们配置您的测试以在 GitHub Actions 中自动运行,确保每次代码更改时您的购买逻辑都保持可靠。
步骤 1:创建 GitHub Actions 工作流
创建 .github/workflows/ios-tests.yml:
name: iOS Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
test:
runs-on: macos-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Select Xcode version
run: sudo xcode-select -s /Applications/Xcode.app
- name: Cache SPM packages
uses: actions/cache@v3
with:
path: |
~/Library/Developer/Xcode/DerivedData
.build
key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
restore-keys: |
${{ runner.os }}-spm-
- name: Run unit tests
env:
REVENUECAT_TEST_STORE_API_KEY: ${{ secrets.REVENUECAT_TEST_STORE_API_KEY }}
run: |
xcodebuild test \
-scheme MyApp \
-destination 'platform=iOS Simulator,name=iPhone 15,OS=latest' \
-resultBundlePath TestResults.xcresult \
REVENUECAT_API_KEY=$REVENUECAT_TEST_STORE_API_KEY
- name: Upload test results
if: always()
uses: actions/upload-artifact@v3
with:
name: test-results
path: TestResults.xcresult步骤 2:将 Test Store API 密钥添加到 GitHub Secrets
- 转到您的 GitHub 仓库
- 导航到 Settings → Secrets and variables → Actions
- 点击 New repository secret
- 名称:
REVENUECAT_TEST_STORE_API_KEY - 值:您的 Test Store API 密钥(例如
test_xxxxx) - 点击 Add secret
步骤 3:配置 Xcode 使用 Secret
更新您的 xcconfig 以在可用时读取环境变量:
// Debug.xcconfig
// 如果可用则使用环境变量(CI),否则回退到本地密钥
REVENUECAT_API_KEY = $(REVENUECAT_API_KEY:default=test_YOUR_KEY_HERE)步骤 4:Fastlane 替代方案
如果您使用 Fastlane 进行 iOS CI/CD,您可以添加一个测试 lane:
lane :test do
run_tests(
scheme: "MyApp",
devices: ["iPhone 15"],
result_bundle: true,
xcargs: "REVENUECAT_API_KEY=#{ENV['REVENUECAT_TEST_STORE_API_KEY']}"
)
end验证您的设置
提交并推送您的更改以触发工作流。前往 GitHub 仓库的 Actions 标签页观看您的测试自动运行。您会看到实时进度,工作流检出您的代码、设置 macOS 环境并运行测试套件。几分钟内,您就会知道所有测试是否在 CI 环境中通过。
自动化测试的力量
一旦设置完成,每个拉取请求在合并前都会自动验证。如果任何更改破坏了您的购买逻辑,您会立即收到反馈,无需手动测试每个场景。Test Store 确保您的测试在每次运行中产生一致的结果,消除了不稳定测试的烦恼。这创建了一个质量门禁,防止有 bug 的代码进入您的主分支,同时节省了大量原本用于重复手动测试的时间。
最佳实践与生产环境设置
0:03:00现在您已经设置了 Test Store,让我们介绍最佳实践以及如何在 Test Store 和生产环境之间切换。
环境分离
始终使用 #if DEBUG 保持 Test Store 和生产密钥分开:
enum RevenueCatKeys {
// Test Store - 仅用于测试
static let testStoreKey = "test_xxxxx"
// App Store - 用于生产
static let appStoreKey = "appl_xxxxx"
static var apiKey: String {
#if DEBUG
return testStoreKey
#else
return appStoreKey
#endif
}
}Xcode Scheme/Configuration 密钥切换
使用 Xcode 构建配置实现更精细的控制:
// Debug.xcconfig (Test Store)
REVENUECAT_API_KEY = test_xxxxx
USE_TEST_STORE = YES
// Staging.xcconfig (Test Store)
REVENUECAT_API_KEY = test_xxxxx
USE_TEST_STORE = YES
// Release.xcconfig (App Store)
REVENUECAT_API_KEY = appl_xxxxx
USE_TEST_STORE = NO构建全面的测试策略
应用内购买的健全测试策略应该遵循金字塔方法。首先使用 Test Store 进行单元测试,以在隔离环境中验证您的购买逻辑。这些测试运行快速,能够及早发现大多数问题。在此基础上使用 Test Store 构建集成测试,以端到端验证完整的购买流程。一旦您对实现有信心,就转向 Apple Sandbox 进行手动测试以验证平台集成。最后,用真实支付进行小规模生产测试,以确保一切在实际环境中正常工作。
在 Test Store 和 Apple Sandbox 之间选择
在初始开发和迭代过程中,Test Store 是您最好的朋友。它非常适合快速原型设计购买流程、编写单元和集成测试以及测试错误处理和边缘情况。即时反馈循环使其成为需要可靠、快速结果的 CI/CD 自动化测试的理想选择。
当您需要测试 Test Store 无法模拟的平台特定功能时,切换到 Apple Sandbox。这包括待处理交易(如等待"询问购买"批准的交易)、收据验证细节、订阅续订周期和区域特定定价。将 Sandbox 视为您的预生产验证环境。在您的核心逻辑稳固并需要验证 App Store 集成后使用它。
安全最佳实践
永远不要在生产环境中暴露您的 Test Store API 密钥:
// ❌ 错误:硬编码密钥
let apiKey = "test_xxxxx"
// ✅ 正确:编译时配置
#if DEBUG
let apiKey = "test_xxxxx"
#else
let apiKey = "appl_xxxxx"
#endif
// ✅ 更好:通过 Info.plist 从 xcconfig 读取
let apiKey = Bundle.main.infoDictionary?["RevenueCatAPIKey"] as! String日志和调试
启用适当的日志级别:
#if DEBUG
Purchases.logLevel = .verbose
print("RevenueCat: Using Test Store for testing")
#else
Purchases.logLevel = .info
#endif过渡到生产环境
在发布您的应用之前,请确保您已完成彻底的检查清单。所有测试都应该通过 Test Store,并且您应该已经手动验证了与 Apple Sandbox 的集成。仔细检查您的 Xcode 配置是否正确设置以在每个环境中使用正确的 API 密钥。生产 API 密钥必须安全存储,永远不要提交到版本控制。验证 Test Store 密钥已从发布构建中完全删除;它们只应存在于调试配置中。确保日志已适当配置(调试时详细,生产时最小),并确认您的错误处理已在所有场景中测试过。
总结
恭喜!您已成功为 iOS 设置了 RevenueCat 的 Test Store。在本 Codelab 中,您学习了如何在 RevenueCat 仪表板中启用 Test Store、在 iOS 应用程序中配置 Test Store API 密钥、使用确定性结果测试购买流程、为购买逻辑编写自动化单元测试、在 GitHub Actions 等 CI/CD 环境中运行这些测试,以及遵循环境分离的最佳实践。
关键要点
Test Store 从根本上改变了您处理应用内购买测试的方式。不再需要与沙盒账户搏斗和处理不稳定的网络依赖测试。通过对购买结果的确定性控制,您可以编写可靠的测试,让您对自己的实现真正有信心。CI/CD 集成意味着每次代码更改都会持续验证您的购买逻辑,使您能够在几秒钟内而不是几小时内迭代购买流程。最重要的是,您将在购买相关的 bug 到达生产环境之前捕获它们,保护您的收入和用户体验。
下一步
现在您已经设置了 Test Store,是时候在此基础上构建了。首先编写覆盖所有购买场景的全面测试。不要只测试正常路径。将这些测试集成到您的 CI/CD 流水线中,以便在每个拉取请求上自动运行。使用不同的失败模式彻底测试您的错误处理,以确保您的应用能够优雅降级。当您准备好后,过渡到 Apple Sandbox 进行平台特定测试,以验证订阅续订等功能。最后,满怀信心地发布,因为您知道您的购买逻辑在每个步骤都经过了彻底验证。
其他资源
RevenueCat Test Store 博客文章 RevenueCat iOS SDK 文档 测试指南 GitHub: purchases-ios祝测试愉快!