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 系统。这使您能够快速迭代购买流程,在几秒钟内而不是几分钟或几小时内测试更改。

overview

前提条件

在开始之前,请确保您具备:

  1. 一个 RevenueCat 账户(在 revenuecat.com 免费注册)
  2. 已集成 RevenueCat SDK(5.x 或更高版本)的 iOS 项目
  3. SwiftiOS 开发的基础知识
  4. 熟悉 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 StoreEnable 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-setup

下一步

现在您已经有了 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 的文件:

text
// Debug.xcconfig
REVENUECAT_API_KEY = test_YOUR_KEY_HERE

和一个 Release.xcconfig

text
// Release.xcconfig
REVENUECAT_API_KEY = appl_YOUR_PRODUCTION_KEY

然后将 REVENUECAT_API_KEY 添加到您的 Info.plist

xml
<key>RevenueCatAPIKey</key>
<string>$(REVENUECAT_API_KEY)</string>

步骤 2:在应用中初始化 SDK

更新您的 @main App 结构体以使用适当的 API 密钥配置 RevenueCat:

swift
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 预处理器指令创建一个配置助手:

swift
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
    }
}

然后在应用初始化中使用它:

swift
@main
struct MyApp: App {
    init() {
        RevenueCatConfig.configure()
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

验证

要验证 Test Store 是否正常工作:

  1. 在 Xcode 中以调试模式运行您的应用
  2. 检查 Xcode 控制台中是否有 "Purchases SDK initialized with Test Store"
  3. SDK 会指示它连接到 Test Store 而不是 App Store
test-store-config

交互式测试购买流程

0:05:00

启用 Test Store 后,您现在可以完全控制购买流程的结果来进行测试。当 Test Store 显示其购买对话框时,决定结果。

理解 Test Store 购买对话框

当您使用 Test Store 发起购买时,您会看到一个自定义的 Test Store 对话框,而不是标准的 App Store 购买界面。此对话框像真正的购买对话框一样显示产品详情和价格,但有一个关键区别:您可以控制结果。对话框提供三个明确的选项:购买成功模拟完成的交易,购买失败测试支付失败,以及取消模拟用户取消。这种确定性行为正是 Test Store 如此强大的原因。

测试成功流程

让我们测试一个成功的购买流程:

swift
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 对话框出现时,点击"购买成功"。您的应用应该立即授予高级访问权限,您可以验证权益是否正确更新。这让您可以快速验证成功流程是否正常工作,无需真实的支付方式或沙盒账户。

测试失败场景

现在测试您的应用如何处理失败:

swift
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 对话框出现时,点击"购买失败"。您的应用应该优雅地处理错误,向用户显示适当的错误消息。确认用户没有获得高级访问权限,并且应用的状态保持一致。这对于确保您的错误处理逻辑在生产环境中正确工作至关重要。

测试取消

swift
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 集成时,您将对自己的购买处理逻辑非常有信心。

test-store-testing

编写自动化单元测试

0:08:00

Test Store 最强大的功能之一是支持您的应用内购买逻辑的自动化单元测试。让我们编写能够在 CI/CD 中可靠运行的测试。

步骤 1:设置 XCTest

XCTest 内置于 Xcode 中,因此无需外部依赖项。只需确保您的项目有一个测试目标。如果您使用了 Xcode 的默认项目模板,则已经包含了一个。您的 Package.swift 或 Xcode 项目应该已经将 RevenueCat 作为依赖项:

swift
// 在您的 Package.swift 或通过 Xcode SPM
dependencies: [
    .package(url: "https://github.com/RevenueCat/purchases-ios.git", from: "5.58.0")
]

步骤 2:创建购买仓库协议

首先,让我们使用 Swift 协议创建一个可测试的购买仓库:

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

swift
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 编写单元测试

现在使用模拟仓库创建单元测试:

swift
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 的测试替代方案

为了获得更多控制,您可以使用完全模拟的方法:

swift
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

yaml
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(&#039;**/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 &#039;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

  1. 转到您的 GitHub 仓库
  2. 导航到 SettingsSecrets and variablesActions
  3. 点击 New repository secret
  4. 名称:REVENUECAT_TEST_STORE_API_KEY
  5. 值:您的 Test Store API 密钥(例如 test_xxxxx
  6. 点击 Add secret

步骤 3:配置 Xcode 使用 Secret

更新您的 xcconfig 以在可用时读取环境变量:

text
// Debug.xcconfig
// 如果可用则使用环境变量(CI),否则回退到本地密钥
REVENUECAT_API_KEY = $(REVENUECAT_API_KEY:default=test_YOUR_KEY_HERE)

步骤 4:Fastlane 替代方案

如果您使用 Fastlane 进行 iOS CI/CD,您可以添加一个测试 lane:

ruby
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 和生产密钥分开:

swift
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 构建配置实现更精细的控制:

text
// 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 密钥:

swift
// ❌ 错误:硬编码密钥
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

日志和调试

启用适当的日志级别:

swift
#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

祝测试愉快!