Back to the RevenueCat homepage
RevenueCat SDKGoogle Play Billing

Chapter 16: Testing

Testing billing from scratch means setting up license tester accounts, navigating Play Console test tracks, and dealing with shortened subscription intervals that still require real Google Play sandbox flows. RevenueCat gives you a faster alternative: the Test Store.

What Is the Test Store

The Test Store is a RevenueCat-managed testing environment that runs entirely without Google Play. When your app is configured with a test_ API key, calling awaitPurchase() shows a local dialog instead of launching the Google Play purchase sheet:

  • Successful Purchase, SDK returns a successful PurchaseResult with active entitlements
  • Failed Purchase, SDK throws a PurchasesTransactionException with a payment failure error
  • Cancel, SDK throws with userCancelled = true

No sandbox accounts. No test tracks. No network dependency on Google's servers. Every outcome is deterministic.

Setup

1. Enable the Test Store

In the RevenueCat dashboard → your app → Apps & providersCreate Test Store. This generates a separate API key prefixed with test_.

2. Use the Test Key in Debug Builds

kotlin
// build.gradle.kts
android {
    buildTypes {
        debug {
            buildConfigField("String", "RC_API_KEY", "\"test_YOUR_TEST_KEY_HERE\"")
        }
        release {
            buildConfigField("String", "RC_API_KEY", "\"goog_YOUR_PRODUCTION_KEY_HERE\"")
        }
    }
}

3. Initialize with the Build Config Key

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()
        )
    }
}

That is all the setup required. The same awaitPurchase() call you write for production will show the Test Store dialog in debug builds.

Testing Each Purchase Outcome

The Test Store dialog lets you select any outcome before your code runs: successful purchase, cancellation, or specific error codes like BILLING_UNAVAILABLE and ITEM_ALREADY_OWNED. This means you can exercise every branch of your error handling code without needing to trigger real billing failures.

kotlin
// In your paywall or test, this call triggers the Test Store dialog in debug builds
try {
    val result = Purchases.sharedInstance.awaitPurchase(
        PurchaseParams.Builder(activity, pkg).build()
    )
    val customerInfo = result.customerInfo
    val isActive = customerInfo.entitlements["pro_access"]?.isActive == true
    // → Test: select "Successful Purchase" in the dialog

} catch (e: PurchasesTransactionException) {
    if (e.userCancelled) {
        // → Test: select "Cancel" in the dialog
    } else {
        showError(e.error.message)
        // → Test: select "Failed Purchase" in the dialog
    }
}

Unit Testing

For unit tests, wrap Purchases behind an interface so you can mock it without the SDK. The Purchases singleton is not directly mockable, and unit tests should not depend on a real SDK instance or network. Introducing a thin BillingService interface between your ViewModels and the SDK lets you inject a fake in tests and verify your business logic independently of RevenueCat's internals.

kotlin
interface BillingService {
    suspend fun getOfferings(): Offerings
    suspend fun purchase(activity: Activity, pkg: Package): CustomerInfo
    suspend fun getCustomerInfo(): CustomerInfo
}

class RevenueCatBillingService : BillingService {
    override suspend fun getOfferings() =
        Purchases.sharedInstance.awaitOfferings()

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

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

Your ViewModel takes a BillingService. In tests, inject a mock:

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

    @Test
    fun `purchase success grants access`() = runTest {
        val mockInfo = mockk<CustomerInfo> {
            every { entitlements["pro_access"]?.isActive } returns true
        }
        coEvery { billing.purchase(any(), any()) } returns mockInfo

        viewModel.purchase(mockActivity, mockPackage)

        assertTrue(viewModel.state.value is PaywallState.Success)
    }

    @Test
    fun `purchase failure shows error`() = runTest {
        coEvery { billing.purchase(any(), any()) } throws PurchasesException(
            PurchasesError(PurchasesErrorCode.StoreProblemError)
        )

        viewModel.purchase(mockActivity, mockPackage)

        assertTrue(viewModel.state.value is PaywallState.Error)
    }
}

CI/CD

The Test Store works without a device or Google Play account, which makes it suitable for automated test runs in CI. Because the Test Store API key is distinct from your production key, you can safely expose it to your CI environment without risking live purchases. Store it as a repository secret and inject it at build time via a BuildConfigField. Your unit tests then run against the mock billing layer, while instrumented tests can drive the Test Store dialog programmatically.

kotlin
// build.gradle.kts
val testKey = System.getenv("RC_TEST_STORE_KEY") ?: "test_placeholder"
buildConfigField("String", "RC_API_KEY", "\"$testKey\"")
yaml
# .github/workflows/test.yml
- name: Run tests
  env:
    RC_TEST_STORE_KEY: ${{ secrets.RC_TEST_STORE_KEY }}
  run: ./gradlew testDebugUnitTest

When to Use Google Play Sandbox Instead

The Test Store covers most development and CI testing. Use the Google Play Sandbox when you specifically need to test:

Scenario

Use

Purchase success / failure / cancel

Test Store

Unit and integration tests

Test Store

CI/CD automated tests

Test Store

Subscription renewal cycles

Google Play Sandbox

Pending purchase (parental approval)

Google Play Sandbox

Actual payment flow end-to-end

Google Play Sandbox

Debug Dashboard

After any purchase (Test Store or Sandbox), inspect the result in the RevenueCat dashboard under Customers → [user ID]: full purchase history, active entitlements, and raw CustomerInfo JSON. The Events tab shows every webhook sent. This is the fastest way to confirm a test purchase was recorded correctly.

Pre-Ship Checklist

  • Purchases.logLevel = LogLevel.DEBUG removed or gated on BuildConfig.DEBUG
  • Release build type uses production goog_ API key, not test_ key
  • test_ key is not committed to version control (use env variable or local properties)
  • Happy path, error path, and cancellation tested via Test Store dialog
  • At least one end-to-end flow verified via Google Play Sandbox
  • Webhook endpoint receives events for sandbox purchases

Prefer building from scratch?

The Google Play Billing Handbook covers the same topics with raw BillingClient, Developer API, and RTDNs.

Related chapters

  • Chapter 6: The Purchase Flow

    awaitPurchase() handles the complete billing flow, verification, and acknowledgement internally.

    Learn more
  • Chapter 5: Configuring the SDK

    A single configure() call replaces the entire connection lifecycle, reconnection, and sync logic.

    Learn more
  • Chapter 8: Error Handling

    PurchasesErrorCode replaces BillingResponseCode. The SDK handles all retry logic automatically.

    Learn more
Testing | RevenueCat