rc-testing

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Testing RevenueCat on Android

在Android上测试RevenueCat

Use this skill to stand up a fast test loop for a RevenueCat Android integration. The Test Store replaces the Google Play sandbox for most work, a
BillingService
interface makes
Purchases
mockable, and a small GitHub Actions job runs unit tests on every push.
Full source: see the full chapter on revenuecat.com.

使用此技能可为RevenueCat Android集成搭建快速测试循环。Test Store可替代Google Play沙箱完成大部分工作,
BillingService
接口让
Purchases
可被模拟,一个轻量的GitHub Actions任务会在每次代码推送时运行单元测试。
完整源码:查看revenuecat.com上的完整章节

Phase 1: Discovery

阶段1:现状调研

Before changing anything, learn what already exists. Answer each question with a file or a "no".
CheckWhere to lookWhat you want
Test API key configured?
app/build.gradle.kts
,
local.properties
, env
A
test_...
value bound to
BuildConfig.RC_API_KEY
in debug
Release key separate?
build.gradle.kts
release block
A
goog_...
key, never
test_...
Purchases wrapped?
app/src/main/java/**/*Billing*.kt
An interface that hides
Purchases.sharedInstance
CI present?
.github/workflows/*.yml
A job running
./gradlew testDebugUnitTest
Secret wired?GitHub repo settings, workflow
env:
RC_TEST_STORE_KEY
referenced from
secrets
Test library?
app/build.gradle.kts
dependencies
io.mockk:mockk
and
kotlinx-coroutines-test
If the project has none of these, start from scratch in Phase 3. If some exist, only fill the gaps.

在进行任何修改前,先了解现有配置。用文件路径或“无”回答每个问题。
检查项查看位置预期结果
是否配置了测试API密钥?
app/build.gradle.kts
local.properties
、环境变量
debug构建中
BuildConfig.RC_API_KEY
绑定了
test_...
格式的值
是否区分发布密钥?
build.gradle.kts
的release配置块
使用
goog_...
格式的密钥,绝对不能用
test_...
格式
是否封装了Purchases?
app/src/main/java/**/*Billing*.kt
存在隐藏
Purchases.sharedInstance
的接口
是否有CI配置?
.github/workflows/*.yml
存在运行
./gradlew testDebugUnitTest
的任务
是否配置了密钥?GitHub仓库设置、工作流
env:
secrets
中引用
RC_TEST_STORE_KEY
是否引入测试库?
app/build.gradle.kts
的依赖项
包含
io.mockk:mockk
kotlinx-coroutines-test
如果项目完全没有上述配置,从阶段3开始从头搭建。如果已有部分配置,仅补充缺失项即可。

Phase 2: Plan

阶段2:测试规划

Pick the test path per scenario. The Test Store is the default; fall back to Google Play sandbox only when you need store behavior the Test Store does not simulate.
ScenarioUse
Success, failure, cancel pathsTest Store
Unit tests of ViewModelsmockk against
BillingService
CI on every pushTest Store + unit tests
Subscription renewal cyclesGoogle Play Sandbox
Pending purchase (parental approval)Google Play Sandbox
Full end-to-end paymentGoogle Play Sandbox
Write the plan as a short checklist in
tasks/todo.md
before coding. If the app ships subscriptions, keep one Sandbox pass in the pre-ship checklist even if the Test Store covers daily iteration.

根据场景选择测试路径。默认使用Test Store;仅当需要Test Store无法模拟的商店行为时,才退而使用Google Play沙箱。
场景使用方案
成功、失败、取消流程Test Store
ViewModel单元测试针对
BillingService
使用mockk
每次推送触发CITest Store + 单元测试
订阅续订周期Google Play沙箱
待处理购买(家长审批)Google Play沙箱
完整端到端支付Google Play沙箱
在编码前,将规划写成简短的 checklist 存入
tasks/todo.md
。如果应用包含订阅功能,即使Test Store覆盖了日常迭代,预发布检查清单中也要保留一次沙箱验证流程。

Phase 3: Execute

阶段3:执行搭建

Step 1: Generate the test API key

步骤1:生成测试API密钥

In the RevenueCat dashboard, open your app, go to Apps & providers, then Create Test Store. Copy the
test_...
key. Do not commit it. Put it in
local.properties
or a CI secret.
在RevenueCat控制台中打开你的应用,进入Apps & providers,然后点击Create Test Store。复制
test_...
格式的密钥。请勿提交该密钥,将其存入
local.properties
或CI密钥中。

Step 2: Wire the key into debug builds

步骤2:将密钥接入debug构建

kotlin
// build.gradle.kts
android {
    buildTypes {
        debug {
            buildConfigField("String", "RC_API_KEY", "\"test_YOUR_KEY\"")
        }
        release {
            buildConfigField("String", "RC_API_KEY", "\"goog_YOUR_KEY\"")
        }
    }
}
For CI, read from env so the key never lands in git:
kotlin
// build.gradle.kts
val testKey = System.getenv("RC_TEST_STORE_KEY") ?: "test_placeholder"
buildConfigField("String", "RC_API_KEY", "\"$testKey\"")
kotlin
// build.gradle.kts
android {
    buildTypes {
        debug {
            buildConfigField("String", "RC_API_KEY", "\"test_YOUR_KEY\"")
        }
        release {
            buildConfigField("String", "RC_API_KEY", "\"goog_YOUR_KEY\"")
        }
    }
}
对于CI环境,从环境变量读取密钥,避免密钥存入git:
kotlin
// build.gradle.kts
val testKey = System.getenv("RC_TEST_STORE_KEY") ?: "test_placeholder"
buildConfigField("String", "RC_API_KEY", "\"$testKey\"")

Step 3: Configure Purchases with the build config key

步骤3:用构建配置密钥初始化Purchases

kotlin
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.DEBUG) Purchases.logLevel = LogLevel.DEBUG
        Purchases.configure(
            PurchasesConfiguration.Builder(this, BuildConfig.RC_API_KEY).build()
        )
    }
}
kotlin
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.DEBUG) Purchases.logLevel = LogLevel.DEBUG
        Purchases.configure(
            PurchasesConfiguration.Builder(this, BuildConfig.RC_API_KEY).build()
        )
    }
}

Step 4: Trigger the Test Store dialog

步骤4:触发Test Store对话框

Run the app in debug and call
awaitPurchase()
from your paywall. A dialog appears with Success, Fail, and Cancel options. Each choice resolves the same way production would: Success returns a
PurchaseResult
with active entitlements, Fail throws
PurchasesTransactionException
with a payment error, Cancel throws with
userCancelled = true
. Walk every branch of your error handling to verify UI states.
以debug模式运行应用,从付费墙调用
awaitPurchase()
。此时会弹出包含成功、失败、取消选项的对话框。每个选项的处理逻辑与生产环境一致:成功返回带有有效权益的
PurchaseResult
,失败抛出带有支付错误的
PurchasesTransactionException
,取消抛出
userCancelled = true
的异常。遍历所有错误处理分支,验证UI状态是否正确。

Step 5: Wrap Purchases in a BillingService

步骤5:用BillingService封装Purchases

The
Purchases
singleton is not mockable. A thin interface lets you inject a fake in tests and keeps ViewModels free of SDK types.
kotlin
interface BillingService {
    suspend fun getOfferings(): Offerings
    suspend fun purchase(activity: Activity, pkg: Package): CustomerInfo
    suspend fun getCustomerInfo(): CustomerInfo
}
kotlin
class RevenueCatBillingService : BillingService {
    override suspend fun getOfferings() =
        Purchases.sharedInstance.awaitOfferings()

    override suspend fun purchase(activity: Activity, pkg: Package): CustomerInfo =
        Purchases.sharedInstance.awaitPurchase(
            PurchaseParams.Builder(activity, pkg).build()
        ).customerInfo

    override suspend fun getCustomerInfo() =
        Purchases.sharedInstance.awaitCustomerInfo()
}
Purchases
单例无法被模拟。一个轻量接口可让你在测试中注入假实现,同时让ViewModel摆脱SDK类型依赖。
kotlin
interface BillingService {
    suspend fun getOfferings(): Offerings
    suspend fun purchase(activity: Activity, pkg: Package): CustomerInfo
    suspend fun getCustomerInfo(): CustomerInfo
}
kotlin
class RevenueCatBillingService : BillingService {
    override suspend fun getOfferings() =
        Purchases.sharedInstance.awaitOfferings()

    override suspend fun purchase(activity: Activity, pkg: Package): CustomerInfo =
        Purchases.sharedInstance.awaitPurchase(
            PurchaseParams.Builder(activity, pkg).build()
        ).customerInfo

    override suspend fun getCustomerInfo() =
        Purchases.sharedInstance.awaitCustomerInfo()
}

Step 6: Write mockk unit tests

步骤6:编写mockk单元测试

kotlin
class PaywallViewModelTest {
    private val billing = mockk<BillingService>()
    private val viewModel = PaywallViewModel(billing)

    @Test
    fun `purchase success grants access`() = runTest {
        val info = mockk<CustomerInfo> {
            every { entitlements["pro_access"]?.isActive } returns true
        }
        coEvery { billing.purchase(any(), any()) } returns info
        viewModel.purchase(mockActivity, mockPackage)
        assertTrue(viewModel.state.value is PaywallState.Success)
    }
}
Add one test per branch: success grants access, failure shows error, cancel stays idle.
kotlin
class PaywallViewModelTest {
    private val billing = mockk<BillingService>()
    private val viewModel = PaywallViewModel(billing)

    @Test
    fun `purchase success grants access`() = runTest {
        val info = mockk<CustomerInfo> {
            every { entitlements["pro_access"]?.isActive } returns true
        }
        coEvery { billing.purchase(any(), any()) } returns info
        viewModel.purchase(mockActivity, mockPackage)
        assertTrue(viewModel.state.value is PaywallState.Success)
    }
}
为每个分支编写测试:成功授予权限、失败显示错误、取消保持空闲状态。

Step 7: Add a minimal GitHub Actions job

步骤7:添加轻量GitHub Actions任务

yaml
undefined
yaml
undefined

.github/workflows/test.yml

.github/workflows/test.yml

name: test on: [push, pull_request] jobs: unit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: distribution: temurin java-version: 17 - name: Run tests env: RC_TEST_STORE_KEY: ${{ secrets.RC_TEST_STORE_KEY }} run: ./gradlew testDebugUnitTest

Store the `test_...` key as the `RC_TEST_STORE_KEY` repository secret. The job runs without a device or a Google account.

---
name: test on: [push, pull_request] jobs: unit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 with: distribution: temurin java-version: 17 - name: Run tests env: RC_TEST_STORE_KEY: ${{ secrets.RC_TEST_STORE_KEY }} run: ./gradlew testDebugUnitTest

将`test_...`格式的密钥存储为仓库密钥`RC_TEST_STORE_KEY`。该任务无需设备或Google账号即可运行。

---

Verification

验证

After each purchase, open the RevenueCat dashboard, go to Customers, pick the user, and confirm the purchase, the entitlement, and the
CustomerInfo
JSON. The Events tab shows webhooks fired for the test purchase.
每次测试购买后,打开RevenueCat控制台,进入Customers,选择用户,确认购买记录、权益以及
CustomerInfo
JSON。Events标签页会显示测试购买触发的Webhook事件。

Pre-Ship Checklist

预发布检查清单

  • Purchases.logLevel = LogLevel.DEBUG
    gated on
    BuildConfig.DEBUG
  • Release build uses the
    goog_
    key, not the
    test_
    key
  • test_
    key is not committed (env var or
    local.properties
    )
  • Success, failure, and cancel paths exercised through the Test Store dialog
  • At least one end-to-end flow verified in Google Play Sandbox
  • Webhook endpoint receives events for sandbox purchases
  • Purchases.logLevel = LogLevel.DEBUG
    仅在
    BuildConfig.DEBUG
    下启用
  • 发布构建使用
    goog_
    格式密钥,而非
    test_
    格式
  • test_
    格式密钥未提交至git(使用环境变量或
    local.properties
  • 通过Test Store对话框验证了成功、失败、取消流程
  • 至少在Google Play沙箱中验证了一次完整端到端流程
  • Webhook端点能接收沙箱购买事件

References

参考资料