Test Storeの概要

0:02:00

RevenueCat Test StoreのAndroidセットアップガイドへようこそ!

アプリ内課金のテストは常に困難な作業でした。サンドボックスアカウントの設定、テスト商品の作成、アプリ審査の待機、不安定なネットワーク状況への対応が必要です。RevenueCatのTest Storeは、決定論的で高速かつ信頼性の高いテスト環境を提供することで、これらの課題を解消します。

Test Storeとは?

Test Storeは、Google Playストアに接続せずにアプリ内課金フローをテストできるRevenueCatの組み込みテスト環境です。すべての新しいRevenueCatプロジェクトで自動的にプロビジョニングされ、購入結果を完全に制御できます。

学習内容

このコードラボでは、RevenueCatのTest Storeをセットアップして使用するための完全なプロセスをガイドします。RevenueCatダッシュボードでTest Storeを有効にし、AndroidアプリケーションをTest Store APIキーを使用するように設定します。その後、決定論的な結果で購入フローをテストする方法、アプリ内課金ロジックの自動ユニットテストの作成方法、GitHub Actionsを使用してこれらのテストをCI/CDパイプラインに統合する方法を学びます。

Test Storeの利点

Test Storeはアプリ内課金のテスト方法を革新します。Google Play設定やアプリ承認を待つ必要がある従来のテスト方法とは異なり、Test Storeは即座のテスト機能を提供します。購入結果を完全に決定論的に制御できます:トランザクションが成功するか、失敗するか、キャンセルされるかは完全にあなた次第です。これにより、不安定なネットワーク接続や信頼性の低いサンドボックス環境に対処する必要がなくなります。

真の力は、自動テストを書き始めるときに発揮されます。Test Storeを使用すると、GitHub ActionsなどのCI/CDシステムを含む任意の環境で一貫して実行される、購入ロジックの信頼性の高いユニットテストを構築できます。これにより、数分や数時間ではなく、数秒で購入フローを反復できます。

overview

前提条件

開始する前に、以下を確認してください:

  1. RevenueCatアカウントrevenuecat.comで無料)
  2. RevenueCat SDKを統合したAndroidプロジェクト(バージョン9.0.0以降)
  3. KotlinAndroid開発の基本知識
  4. Jetpack Composeの知識(オプション、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キーと同じように機能しますが、重要な違いが1つあります:すべての購入リクエストをGoogle Playではなく Test Storeにルーティングし、テスト環境を完全に制御できます。

APIキーの分離について: Test Store APIキーは本番環境およびサンドボックスキーとは完全に分離されています。この分離は意図的であり、セキュリティのために重要です。本番ビルドでTest Storeキーを使用しないでください。デバッグおよびテスト環境でのみ使用する必要があります。必要に応じて、異なるテストシナリオ用に複数のTest Store設定を作成できます。

test-store-setup

次のステップ

Test Store APIキーを取得したので、AndroidアプリケーションをTest Storeを使用するように設定する準備ができました。

AndroidアプリでTest Storeを設定する

0:05:00

AndroidアプリケーションをTest Storeを使用するように設定しましょう。重要なのは、テスト実行時に本番APIキーの代わりにTest Store APIキーを使用することです。

ステップ1:BuildConfigフィールドを作成

アプリのbuild.gradle.ktsにTest Store APIキー設定を追加します:

kotlin
android {
    defaultConfig {
        // 既存の設定

        // デバッグビルド用のTest Store APIキーを追加
        buildConfigField("String", "REVENUECAT_TEST_STORE_API_KEY", "\"test_YOUR_KEY_HERE\"")
    }

    buildTypes {
        debug {
            // デバッグビルドにはTest Storeを使用
            buildConfigField("String", "REVENUECAT_API_KEY", "\"test_YOUR_KEY_HERE\"")
        }
        release {
            // リリースビルドには本番APIキーを使用
            buildConfigField("String", "REVENUECAT_API_KEY", "\"goog_YOUR_PRODUCTION_KEY\"")
        }
    }
}

ステップ2:Test StoreでSDKを初期化

Applicationクラスを更新して、ビルドタイプに基づいて適切なAPIキーを使用します:

kotlin
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        // RevenueCat SDKを初期化
        val apiKey = BuildConfig.REVENUECAT_API_KEY

        val builder = PurchasesConfiguration.Builder(this, apiKey)
        Purchases.configure(
            builder
                .purchasesAreCompletedBy(PurchasesAreCompletedBy.REVENUECAT)
                .appUserID(null)
                .diagnosticsEnabled(true)
                .build()
        )

        // テストビルドでデバッグログを有効化
        if (BuildConfig.DEBUG) {
            Purchases.logLevel = LogLevel.DEBUG
        }
    }
}

ステップ3:環境固有の設定

より高度なセットアップでは、設定クラスを作成できます:

kotlin
object RevenueCatConfig {
    fun getApiKey(context: Context): String {
        return if (isTestEnvironment()) {
            BuildConfig.REVENUECAT_TEST_STORE_API_KEY
        } else {
            BuildConfig.REVENUECAT_API_KEY
        }
    }

    private fun isTestEnvironment(): Boolean {
        // テスト環境で実行中かどうかを確認
        return try {
            Class.forName("androidx.test.espresso.Espresso")
            true
        } catch (e: ClassNotFoundException) {
            false
        } || BuildConfig.DEBUG
    }
}

初期化で使用します:

kotlin
val apiKey = RevenueCatConfig.getApiKey(this)
Purchases.configure(
    PurchasesConfiguration.Builder(this, apiKey)
        .purchasesAreCompletedBy(PurchasesAreCompletedBy.REVENUECAT)
        .build()
)

検証

Test Storeが動作していることを確認するには:

  1. デバッグモードでアプリを実行
  2. ログで"Purchases SDK initialized with Test Store"を確認
  3. SDKがGoogle Playではなく Test Storeに接続されていることを示します
test-store-config

購入フローをインタラクティブにテストする

0:05:00

Test Storeを有効にすると、結果を完全に制御して購入フローをテストできます。Test Storeが購入ダイアログを表示すると、あなたが結果を決定します。

Test Store購入ダイアログを理解する

Test Storeで購入を開始すると、標準のGoogle Play購入シートではなく、カスタムのTest Storeダイアログが表示されます。このダイアログは実際の購入ダイアログと同様に商品の詳細と価格を表示しますが、重要な違いが1つあります:結果を制御できます。ダイアログには3つの明確なオプションが表示されます:完了したトランザクションをシミュレートする購入成功、支払い失敗をテストする購入失敗、ユーザーのキャンセルをシミュレートするキャンセル。この決定論的な動作がTest Storeをテストに非常に強力にしています。

成功フローのテスト

成功した購入フローをテストしましょう:

kotlin
suspend fun purchaseProduct(activity: Activity) {
    try {
        // Test Storeから商品を取得
        val products = Purchases.sharedInstance.awaitGetProducts(
            productIds = listOf("premium_monthly")
        )

        // 購入を開始
        val purchaseResult = Purchases.sharedInstance.awaitPurchase(
            purchaseParams = PurchaseParams.Builder(
                activity = activity,
                storeProduct = products.first()
            ).build()
        )

        // 結果を確認
        val customerInfo = purchaseResult.customerInfo
        val isPremium = customerInfo.entitlements["premium"]?.isActive == true

        if (isPremium) {
            // 購入成功!
            showPremiumContent()
        }
    } catch (e: PurchasesException) {
        // エラーを処理
        handlePurchaseError(e)
    }
}

ハッピーパスのテスト: アプリを実行し、通常どおり購入フローをトリガーします。Test Storeダイアログが表示されたら、「購入成功」をタップします。アプリは即座にプレミアムアクセスを付与し、Entitlementが正しく更新されたことを確認できます。これにより、実際の支払い方法やサンドボックスアカウントなしで、成功フローが正しく動作することを素早く検証できます。

失敗シナリオのテスト

アプリが失敗を処理する方法をテストします:

kotlin
fun handlePurchaseError(error: PurchasesException) {
    when (error.code) {
        PurchasesErrorCode.PURCHASE_CANCELLED_ERROR -> {
            // ユーザーがキャンセル - エラーを表示しない
            Log.d("Purchase", "ユーザーが購入をキャンセルしました")
        }
        PurchasesErrorCode.PURCHASE_INVALID_ERROR -> {
            // 無効な購入
            showError("この購入は利用できません")
        }
        PurchasesErrorCode.PAYMENT_PENDING_ERROR -> {
            // 支払い保留中(例:親の承認待ち)
            showPendingMessage()
        }
        else -> {
            // その他のエラー
            showError("購入に失敗しました: ${error.message}")
        }
    }
}

エラーハンドリングのテスト: 購入フローを再度トリガーしますが、今回はTest Storeダイアログが表示されたら、「購入失敗」をタップします。アプリはエラーを適切に処理し、ユーザーに適切なエラーメッセージを表示する必要があります。ユーザーがプレミアムアクセスを受けておらず、アプリの状態が一貫していることを確認します。これは、本番環境でエラーハンドリングロジックが正しく動作することを確認するために重要です。

キャンセルのテスト

kotlin
@Composable
fun PaywallScreen(onDismiss: () -> Unit) {
    val scope = rememberCoroutineScope()
    var isPurchasing by remember { mutableStateOf(false) }

    Button(
        onClick = {
            scope.launch {
                isPurchasing = true
                try {
                    purchaseProduct(LocalContext.current as Activity)
                } catch (e: PurchasesException) {
                    if (e.code == PurchasesErrorCode.PURCHASE_CANCELLED_ERROR) {
                        // ユーザーがキャンセル - 単に閉じる
                        onDismiss()
                    }
                }
                isPurchasing = false
            }
        },
        enabled = !isPurchasing
    ) {
        Text(if (isPurchasing) "処理中..." else "購読する")
    }
}

ユーザーキャンセルのテスト: ペイウォールを開き、購読ボタンをタップします。Test Storeダイアログが表示されたら、「キャンセル」をタップしてユーザーが購入を中止することをシミュレートします。ペイウォールはエラーメッセージを表示せずにきれいに閉じる必要があります(キャンセルはエラー状態ではなく、通常のユーザーアクションです)。購入が記録されておらず、アプリの状態が変更されていないことを確認します。

重要なポイント

Test Storeの美しさは、その決定論的な制御にあります。各購入試行で何が起こるかを正確に決定します。これは、数分で成功、失敗、キャンセルのすべてのシナリオを徹底的にテストできることを意味し、実際の支払い方法やサンドボックスアカウントの遅延や複雑さに対処する必要がありません。Google Playと統合する準備ができるまでに、購入処理ロジックが堅固であるという確信を持てます。

test-store-testing

自動ユニットテストを書く

0:08:00

Test Storeの最も強力な機能の1つは、アプリ内課金ロジックの自動ユニットテストを可能にすることです。CI/CDで信頼性高く実行されるテストを書きましょう。

ステップ1:テスト依存関係を追加

build.gradle.ktsに以下を追加します:

kotlin
dependencies {
    // RevenueCat SDK
    implementation("com.revenuecat.purchases:purchases:9.20.2")

    // テスト依存関係
    testImplementation("junit:junit:4.13.2")
    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
    testImplementation("io.mockk:mockk:1.13.8")
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
    androidTestImplementation("androidx.test:runner:1.5.2")
}

ステップ2:購入リポジトリを作成

まず、テスト可能な購入リポジトリを作成しましょう:

kotlin
interface PurchaseRepository {
    suspend fun getProducts(productIds: List<String>): List<StoreProduct>
    suspend fun purchaseProduct(activity: Activity, product: StoreProduct): CustomerInfo
    suspend fun getCustomerInfo(): CustomerInfo
}

class PurchaseRepositoryImpl(
    private val purchases: Purchases = Purchases.sharedInstance
) : PurchaseRepository {

    override suspend fun getProducts(productIds: List<String>): List<StoreProduct> {
        return purchases.awaitGetProducts(productIds)
    }

    override suspend fun purchaseProduct(
        activity: Activity,
        product: StoreProduct
    ): CustomerInfo {
        val result = purchases.awaitPurchase(
            PurchaseParams.Builder(activity, product).build()
        )
        return result.customerInfo
    }

    override suspend fun getCustomerInfo(): CustomerInfo {
        return purchases.awaitCustomerInfo()
    }
}

ステップ3:ビジネスロジックを含むViewModelを作成

kotlin
class PaywallViewModel(
    private val repository: PurchaseRepository
) : ViewModel() {

    private val _state = MutableStateFlow<PaywallState>(PaywallState.Loading)
    val state: StateFlow<PaywallState> = _state.asStateFlow()

    fun loadProducts() {
        viewModelScope.launch {
            try {
                val products = repository.getProducts(listOf("premium_monthly"))
                _state.value = PaywallState.Success(products)
            } catch (e: Exception) {
                _state.value = PaywallState.Error(e.message ?: "不明なエラー")
            }
        }
    }

    fun purchaseProduct(activity: Activity, product: StoreProduct) {
        viewModelScope.launch {
            _state.value = PaywallState.Purchasing
            try {
                val customerInfo = repository.purchaseProduct(activity, product)
                val isPremium = customerInfo.entitlements["premium"]?.isActive == true

                if (isPremium) {
                    _state.value = PaywallState.PurchaseSuccess
                } else {
                    _state.value = PaywallState.Error("購入は完了しましたがEntitlementがアクティブではありません")
                }
            } catch (e: PurchasesException) {
                when (e.code) {
                    PurchasesErrorCode.PURCHASE_CANCELLED_ERROR -> {
                        _state.value = PaywallState.PurchaseCancelled
                    }
                    else -> {
                        _state.value = PaywallState.Error(e.message)
                    }
                }
            }
        }
    }
}

sealed class PaywallState {
    object Loading : PaywallState()
    data class Success(val products: List<StoreProduct>) : PaywallState()
    object Purchasing : PaywallState()
    object PurchaseSuccess : PaywallState()
    object PurchaseCancelled : PaywallState()
    data class Error(val message: String) : PaywallState()
}

ステップ4:ユニットテストを書く

Test Storeを使用してユニットテストを作成します:

kotlin
@RunWith(AndroidJUnit4::class)
class PaywallViewModelTest {

    private lateinit var repository: PurchaseRepository
    private lateinit var viewModel: PaywallViewModel

    @Before
    fun setup() {
        // Test Store APIキーでRevenueCatを初期化
        Purchases.configure(
            PurchasesConfiguration.Builder(
                ApplicationProvider.getApplicationContext(),
                "test_YOUR_KEY_HERE"
            ).build()
        )

        repository = PurchaseRepositoryImpl()
        viewModel = PaywallViewModel(repository)
    }

    @Test
    fun testLoadProducts_Success() = runTest {
        // Given: ViewModelが初期化されている

        // When: 商品を読み込む
        viewModel.loadProducts()

        // 状態が更新されるまで待機
        advanceUntilIdle()

        // Then: 商品が正常に読み込まれるべき
        val state = viewModel.state.value
        assertTrue(state is PaywallState.Success)
        assertFalse((state as PaywallState.Success).products.isEmpty())
    }

    @Test
    fun testPurchaseProduct_Success() = runTest {
        // Given: 商品が読み込まれている
        viewModel.loadProducts()
        advanceUntilIdle()

        val state = viewModel.state.value as PaywallState.Success
        val product = state.products.first()

        // When: ユーザーが商品を購入し、Test Storeダイアログで「購入成功」を表示
        // 注意:実際のテストでは、Test Storeダイアログと対話する必要があります
        viewModel.purchaseProduct(mockActivity, product)
        advanceUntilIdle()

        // Then: 購入が成功し、Entitlementがアクティブになるべき
        val finalState = viewModel.state.value
        assertTrue(finalState is PaywallState.PurchaseSuccess)
    }

    @Test
    fun testPurchaseProduct_Cancelled() = runTest {
        // Given: 商品が読み込まれている
        viewModel.loadProducts()
        advanceUntilIdle()

        val state = viewModel.state.value as PaywallState.Success
        val product = state.products.first()

        // When: ユーザーがTest Storeダイアログで購入をキャンセル
        viewModel.purchaseProduct(mockActivity, product)
        advanceUntilIdle()

        // Then: 状態がキャンセルを示すべき
        val finalState = viewModel.state.value
        assertTrue(finalState is PaywallState.PurchaseCancelled)
    }
}

ステップ5:モックベースのテスト代替案

さらに制御するために、リポジトリをモックできます:

kotlin
class PaywallViewModelMockTest {

    private lateinit var mockRepository: PurchaseRepository
    private lateinit var viewModel: PaywallViewModel

    @Before
    fun setup() {
        mockRepository = mockk()
        viewModel = PaywallViewModel(mockRepository)
    }

    @Test
    fun testPurchaseSuccess() = runTest {
        // Given: 成功した購入をモック
        val mockProduct = mockk<StoreProduct>()
        val mockCustomerInfo = mockk<CustomerInfo> {
            every { entitlements["premium"]?.isActive } returns true
        }

        coEvery {
            mockRepository.purchaseProduct(any(), mockProduct)
        } returns mockCustomerInfo

        // When: 購入が開始される
        viewModel.purchaseProduct(mockk(), mockProduct)
        advanceUntilIdle()

        // Then: 状態が成功であるべき
        assertTrue(viewModel.state.value is PaywallState.PurchaseSuccess)
    }

    @Test
    fun testPurchaseFailed() = runTest {
        // Given: 失敗した購入をモック
        val mockProduct = mockk<StoreProduct>()

        coEvery {
            mockRepository.purchaseProduct(any(), mockProduct)
        } throws PurchasesException(
            PurchasesErrorCode.PAYMENT_PENDING_ERROR,
            "支払いが保留中です"
        )

        // When: 購入が開始される
        viewModel.purchaseProduct(mockk(), mockProduct)
        advanceUntilIdle()

        // Then: 状態がエラーを表示するべき
        val state = viewModel.state.value
        assertTrue(state is PaywallState.Error)
        assertTrue((state as PaywallState.Error).message.contains("保留中"))
    }
}

このアプローチが機能する理由

このテストアプローチは、アプリ内課金テストの従来の課題を解消します。Test Storeにはネットワーク依存関係がないため、テストは毎回信頼性高く実行されます。接続の問題による不安定な失敗はもうありません。テストは数分ではなく数秒で実行され、開発中に素早いフィードバックが得られます。実際の支払いシステムでは再現が困難または時間のかかるエッジケースを含む、すべてのシナリオで完全なテストカバレッジを達成できます。

私たちが構築したアーキテクチャ(ビジネスロジックをSDKから分離するリポジトリパターン)により、これらのテストは高速でメンテナンスしやすくなっています。GitHub Actionsなどの任意のCI/CD環境で実行でき、本番環境に到達する前にバグを発見できます。

CI/CDでテストを実行する

0:04:00

GitHub Actionsでテストを自動的に実行するように設定し、コードの変更ごとに購入ロジックの信頼性を確保しましょう。

ステップ1:GitHub Actionsワークフローを作成

.github/workflows/android-tests.ymlを作成します:

yaml
name: Android Tests

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main, develop ]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: &#039;17'
          distribution: &#039;temurin'

      - name: Cache Gradle packages
        uses: actions/cache@v3
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles(&#039;**__RCPH_0_HPRC__gradle-wrapper.properties') }}
          restore-keys: |
            ${{ runner.os }}-gradle-

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      - name: Run unit tests
        env:
          REVENUECAT_TEST_STORE_API_KEY: ${{ secrets.REVENUECAT_TEST_STORE_API_KEY }}
        run: ./gradlew testDebugUnitTest

      - name: Upload test results
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: test-results
          path: app/build/test-results/

      - name: Upload test reports
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: test-reports
          path: app/build/reports/tests/

ステップ2:Test Store APIキーをGitHub Secretsに追加

  1. GitHubリポジトリに移動
  2. SettingsSecrets and variablesActionsに移動
  3. New repository secretをクリック
  4. Name: REVENUECAT_TEST_STORE_API_KEY
  5. Value: Test Store APIキー(例:test_xxxxx
  6. Add secretをクリック

ステップ3:GradleでSecretを使用するように設定

build.gradle.ktsを更新して環境変数を使用します:

kotlin
android {
    defaultConfig {
        // 環境からTest Store APIキーを取得するか、プレースホルダーを使用
        val testStoreApiKey = System.getenv("REVENUECAT_TEST_STORE_API_KEY")
            ?: "test_placeholder"

        buildConfigField("String", "REVENUECAT_TEST_STORE_API_KEY", "\"$testStoreApiKey\"")
    }
}

ステップ4:エミュレーターでインストルメンテーションテストを実行

より高度なテストのために、インストルメンテーションテストを追加します:

yaml
instrumented-test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up JDK 17
        uses: actions/setup-java@v4
        with:
          java-version: &#039;17'
          distribution: &#039;temurin'

      - name: Enable KVM (for faster emulator)
        run: |
          echo &#039;KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
          sudo udevadm control --reload-rules
          sudo udevadm trigger --name-match=kvm

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      - name: Run instrumented tests
        uses: reactivecircus/android-emulator-runner@v2
        env:
          REVENUECAT_TEST_STORE_API_KEY: ${{ secrets.REVENUECAT_TEST_STORE_API_KEY }}
        with:
          api-level: 29
          target: default
          arch: x86_64
          profile: Nexus 6
          script: ./gradlew connectedDebugAndroidTest

      - name: Upload instrumented test results
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: instrumented-test-results
          path: app/build/outputs/androidTest-results/

セットアップを確認

変更をコミットしてプッシュし、ワークフローをトリガーします。リポジトリのGitHub Actionsタブに移動して、テストが自動的に実行されるのを確認します。ワークフローがコードをチェックアウトし、環境をセットアップし、テストスイートを実行するリアルタイムの進捗状況が表示されます。数分以内に、すべてのテストがCI環境で合格するかどうかがわかります。

自動テストの力

これがセットアップされると、すべてのプルリクエストがマージ前に自動的に検証されます。すべてのシナリオを手動でテストする必要なく、変更が購入ロジックを壊した場合に即座にフィードバックが得られます。Test Storeは、すべての実行で一貫した結果を生成し、不安定なテストの苛立ちを解消します。これにより、バグのあるコードがメインブランチに到達するのを防ぐ品質ゲートが作成され、繰り返しの手動テストに費やされる無数の時間を節約できます。

ベストプラクティスと本番環境セットアップ

0:03:00

Test Storeをセットアップしたので、ベストプラクティスとTest Storeと本番環境間の移行方法について説明しましょう。

環境の分離

常にTest Storeと本番キーを分離してください:

kotlin
object RevenueCatKeys {
    // Test Store - テスト専用
    const val TEST_STORE_KEY = "test_xxxxx"

    // Google Play - 本番用
    const val GOOGLE_PLAY_KEY = "goog_xxxxx"

    fun getApiKey(isTestMode: Boolean): String {
        return if (isTestMode) TEST_STORE_KEY else GOOGLE_PLAY_KEY
    }
}

ビルドバリアント設定

ビルドバリアントを使用して環境を自動的に切り替えます:

kotlin
android {
    buildTypes {
        debug {
            buildConfigField("String", "RC_API_KEY", "\"test_xxxxx\"")
            buildConfigField("Boolean", "USE_TEST_STORE", "true")
        }

        release {
            buildConfigField("String", "RC_API_KEY", "\"goog_xxxxx\"")
            buildConfigField("Boolean", "USE_TEST_STORE", "false")
        }
    }

    flavorDimensions += "environment"
    productFlavors {
        create("dev") {
            dimension = "environment"
            buildConfigField("String", "RC_API_KEY", "\"test_xxxxx\"")
        }

        create("staging") {
            dimension = "environment"
            buildConfigField("String", "RC_API_KEY", "\"test_xxxxx\"")
        }

        create("prod") {
            dimension = "environment"
            buildConfigField("String", "RC_API_KEY", "\"goog_xxxxx\"")
        }
    }
}

包括的なテスト戦略の構築

アプリ内課金の堅牢なテスト戦略は、ピラミッドアプローチに従うべきです。まず、Test Storeを使用したユニットテストで購入ロジックを分離して検証します。これらは高速に実行され、ほとんどの問題を早期に発見します。その基盤の上に、同じくTest Storeを使用した統合テストを構築し、完全な購入フローをエンドツーエンドで検証します。実装に自信が持てたら、Google Play Sandboxでの手動テストに移行してプラットフォーム統合を確認します。最後に、実際の支払いで小規模な本番テストを行い、ライブ環境ですべてが動作することを確認します。

Test StoreとSandboxの選択

初期開発と反復中は、Test Storeが最良の友です。購入フローの迅速なプロトタイピング、ユニットテストと統合テストの作成、エラーハンドリングとエッジケースのテストに最適です。即座のフィードバックループにより、信頼性の高い高速な結果が必要なCI/CD自動テストに理想的です。

Test Storeがシミュレートしないプラットフォーム固有の機能をテストする必要がある場合は、Google Play Sandboxに切り替えます。これには、保留中のトランザクション(親の承認待ちなど)、レシート検証の詳細、サブスクリプション更新サイクル、地域固有の価格設定が含まれます。Sandboxは本番前の検証環境と考えてください。コアロジックが堅固で、Google Play統合を確認する必要があるときに使用します。

セキュリティのベストプラクティス

本番環境でTest Store APIキーを公開しないでください:

kotlin
// ❌ 悪い例:ハードコードされたキー
val apiKey = "test_xxxxx"

// ✅ 良い例:環境ベースの設定
val apiKey = if (BuildConfig.DEBUG) {
    BuildConfig.TEST_STORE_API_KEY
} else {
    BuildConfig.GOOGLE_PLAY_API_KEY
}

// ✅ より良い例:テスト環境をチェック
val apiKey = when {
    isRunningInTests() -> BuildConfig.TEST_STORE_API_KEY
    BuildConfig.DEBUG -> BuildConfig.TEST_STORE_API_KEY
    else -> BuildConfig.GOOGLE_PLAY_API_KEY
}

ロギングとデバッグ

適切なログレベルを有効にします:

kotlin
if (BuildConfig.USE_TEST_STORE) {
    Purchases.logLevel = LogLevel.VERBOSE
    Log.d("RevenueCat", "テスト用にTest Storeを使用しています")
} else if (BuildConfig.DEBUG) {
    Purchases.logLevel = LogLevel.DEBUG
} else {
    Purchases.logLevel = LogLevel.INFO
}

本番への移行

アプリをリリースする前に、徹底的なチェックリストを完了していることを確認してください。すべてのテストがTest Storeで合格し、Google Play Sandboxで統合を手動で確認しておく必要があります。ビルドバリアントが各環境で正しいAPIキーを使用するように適切に設定されていることを再確認してください。本番APIキーはセキュリティで保護され、バージョン管理にコミットされていない必要があります。Test Storeキーがリリースビルドから完全に削除されていることを確認してください。デバッグ設定にのみ存在する必要があります。ロギングが適切に設定されていることを確認し(デバッグでは詳細、本番では最小限)、エラーハンドリングがすべてのシナリオでテストされていることを確認してください。

まとめ

おめでとうございます!🎉 AndroidでRevenueCatのTest Storeを正常にセットアップしました。このコードラボを通じて、RevenueCatダッシュボードでTest Storeを有効にする方法、AndroidアプリケーションでTest Store APIキーを設定する方法、決定論的な結果で購入フローをテストする方法、購入ロジックの自動ユニットテストを作成する方法、GitHub ActionsなどのCI/CD環境でこれらのテストを実行する方法、環境分離のベストプラクティスを学びました。

重要なポイント

Test Storeは、アプリ内課金テストへのアプローチを根本的に変えます。サンドボックスアカウントと格闘したり、ネットワーク依存の不安定なテストに対処する日々は終わりました。購入結果を決定論的に制御することで、実装に真の自信を与える信頼性の高いテストを作成できます。CI/CD統合により、購入ロジックはすべてのコード変更で継続的に検証され、数時間ではなく数秒で購入フローを反復できます。最も重要なのは、購入関連のバグが本番環境に到達する前に発見でき、収益とユーザーエクスペリエンスの両方を保護できることです。

次のステップ

Test Storeをセットアップしたので、この基盤の上に構築する時です。すべての購入シナリオをカバーする包括的なテストを書き始めてください。ハッピーパスだけでなく、すべてのプルリクエストで自動的に実行されるようにCI/CDパイプラインに統合してください。異なる失敗モードでエラーハンドリングを徹底的にテストし、アプリが適切に劣化することを確認してください。準備ができたら、サブスクリプション更新などを確認するためにGoogle Play Sandboxに移行してプラットフォーム固有のテストを行ってください。最後に、購入ロジックがすべてのステップで徹底的に検証されていることを知って、自信を持ってリリースしてください。

追加リソース

RevenueCat Test Storeブログ記事 RevenueCat Android SDKドキュメント テストガイド GitHub: Cat Paywall Compose

Happy testing! 🚀