Test Store 개요

0:02:00

Android용 RevenueCat Test Store 설정 가이드에 오신 것을 환영합니다!

인앱 구매 테스트는 항상 어려웠습니다. 샌드박스 계정을 설정하고, 테스트 상품을 만들고, 앱 검토를 기다리고, 불안정한 네트워크 상태를 처리해야 했습니다. RevenueCat의 Test Store는 결정론적이고 빠르며 신뢰할 수 있는 테스트 환경을 제공하여 이러한 문제점들을 해결합니다.

Test Store란 무엇입니까?

Test Store는 Google Play Store에 연결하지 않고도 인앱 구매 플로우를 테스트할 수 있게 해주는 RevenueCat의 내장 테스트 환경입니다. 모든 새로운 RevenueCat 프로젝트에 자동으로 프로비저닝되며 구매 결과에 대한 완전한 제어를 제공합니다.

학습할 내용

이 codelab에서는 Android 개발을 위한 RevenueCat의 Test Store 설정 및 사용의 전체 과정을 안내합니다. RevenueCat 대시보드에서 Test Store를 활성화하고 Test Store API 키를 사용하도록 Android 애플리케이션을 설정하는 것부터 시작합니다. 그런 다음 결정론적 결과로 구매 플로우를 테스트하고, 인앱 구매 로직에 대한 자동화된 단위 테스트를 작성하고, 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_ 접두사로 시작하는 Test Store API Key를 찾을 수 있습니다. 이 키는 일반 RevenueCat API 키처럼 작동하지만 한 가지 중요한 차이점이 있습니다. 모든 구매 요청을 Google Play 대신 Test Store로 라우팅하여 테스트 환경에 대한 완전한 제어를 제공합니다.

API 키 분리 이해하기: Test Store API 키는 프로덕션 및 샌드박스 키와 완전히 분리되어 있습니다. 이러한 분리는 의도적이며 보안상 중요합니다. 프로덕션 빌드에서는 절대 Test Store 키를 사용하지 마십시오. 디버그 및 테스트 환경에서만 사용해야 합니다. 필요한 경우 다른 테스트 시나리오를 위해 프로젝트당 여러 Test Store 구성을 만들 수 있습니다.

test-store-setup

다음 단계

이제 Test Store API 키를 얻었으므로 테스트용 Test Store를 사용하도록 Android 애플리케이션을 설정할 준비가 되었습니다.

Android 앱에서 Test Store 설정하기

0:05:00

이제 Test Store를 사용하도록 Android 애플리케이션을 설정하겠습니다. 핵심은 테스트를 실행할 때 프로덕션 API 키 대신 Test Store API 키를 사용하는 것입니다.

1단계: BuildConfig 필드 생성하기

앱의 build.gradle.kts에 Test Store API 키 설정을 추가하십시오:

kotlin
android {
    defaultConfig {
        // Your existing configuration

        // Add Test Store API key for debug builds
        buildConfigField("String", "REVENUECAT_TEST_STORE_API_KEY", "\"test_YOUR_KEY_HERE\"")
    }

    buildTypes {
        debug {
            // Use Test Store for debug builds
            buildConfigField("String", "REVENUECAT_API_KEY", "\"test_YOUR_KEY_HERE\"")
        }
        release {
            // Use production API key for release builds
            buildConfigField("String", "REVENUECAT_API_KEY", "\"goog_YOUR_PRODUCTION_KEY\"")
        }
    }
}

2단계: Test Store로 SDK 초기화하기

빌드 유형에 따라 적절한 API 키를 사용하도록 Application 클래스를 업데이트하십시오:

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

        // Initialize 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()
        )

        // Enable debug logs for test builds
        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 {
        // Check if running in test environment
        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 대화 상자가 표시됩니다. 이 대화 상자는 실제 구매 대화 상자처럼 상품 세부 정보와 가격을 표시하지만 한 가지 주요 차이점이 있습니다. 결과를 제어할 수 있습니다. 대화 상자는 완료된 트랜잭션을 시뮬레이션하는 Successful Purchase, 결제 실패를 테스트하는 Failed Purchase, 사용자 취소를 시뮬레이션하는 Cancel의 세 가지 명확한 옵션을 제공합니다. 이러한 결정론적 동작이 Test Store를 테스트에 매우 강력하게 만드는 이유입니다.

성공 플로우 테스트하기

성공적인 구매 플로우를 테스트해 보겠습니다:

kotlin
suspend fun purchaseProduct(activity: Activity) {
    try {
        // Fetch products from Test Store
        val products = Purchases.sharedInstance.awaitGetProducts(
            productIds = listOf("premium_monthly")
        )

        // Initiate purchase
        val purchaseResult = Purchases.sharedInstance.awaitPurchase(
            purchaseParams = PurchaseParams.Builder(
                activity = activity,
                storeProduct = products.first()
            ).build()
        )

        // Check the result
        val customerInfo = purchaseResult.customerInfo
        val isPremium = customerInfo.entitlements["premium"]?.isActive == true

        if (isPremium) {
            // Purchase successful!
            showPremiumContent()
        }
    } catch (e: PurchasesException) {
        // Handle error
        handlePurchaseError(e)
    }
}

해피 패스 테스트: 앱을 실행하고 평소처럼 구매 플로우를 트리거하십시오. Test Store 대화 상자가 나타나면 "Successful Purchase"를 탭하십시오. 앱은 즉시 프리미엄 액세스를 부여해야 하며 권한이 올바르게 업데이트되었는지 확인할 수 있습니다. 이를 통해 실제 결제 수단이나 샌드박스 계정이 없어도 성공 플로우가 올바르게 작동하는지 빠르게 확인할 수 있습니다.

실패 시나리오 테스트하기

이제 앱이 실패를 처리하는 방법을 테스트하십시오:

kotlin
fun handlePurchaseError(error: PurchasesException) {
    when (error.code) {
        PurchasesErrorCode.PURCHASE_CANCELLED_ERROR -> {
            // User cancelled - don't show error
            Log.d("Purchase", "User cancelled purchase")
        }
        PurchasesErrorCode.PURCHASE_INVALID_ERROR -> {
            // Invalid purchase
            showError("This purchase is not available")
        }
        PurchasesErrorCode.PAYMENT_PENDING_ERROR -> {
            // Payment pending (e.g., awaiting parental approval)
            showPendingMessage()
        }
        else -> {
            // Other errors
            showError("Purchase failed: ${error.message}")
        }
    }
}

오류 처리 테스트: 구매 플로우를 다시 트리거하되 이번에는 Test Store 대화 상자가 나타나면 "Failed Purchase"를 탭하십시오. 앱은 오류를 정상적으로 처리하여 사용자에게 적절한 오류 메시지를 표시해야 합니다. 사용자가 프리미엄 액세스를 받지 못하고 앱의 상태가 일관성을 유지하는지 확인하십시오. 이는 프로덕션에서 오류 처리 로직이 올바르게 작동하는지 확인하는 데 중요합니다.

취소 테스트하기

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) {
                        // User cancelled - just dismiss
                        onDismiss()
                    }
                }
                isPurchasing = false
            }
        },
        enabled = !isPurchasing
    ) {
        Text(if (isPurchasing) "Processing..." else "Subscribe")
    }
}

사용자 취소 테스트: 페이월을 열고 구독 버튼을 탭하십시오. Test Store 대화 상자가 나타나면 "Cancel"을 탭하여 구매를 취소하는 사용자를 시뮬레이션하십시오. 페이월은 오류 메시지를 표시하지 않고 깔끔하게 닫혀야 합니다 (취소는 정상적인 사용자 작업이지 오류 상태가 아닙니다). 구매가 기록되지 않았고 앱의 상태가 변경되지 않았는지 확인하십시오.

핵심 요점

Test Store의 장점은 결정론적 제어에 있습니다. 각 구매 시도에서 정확히 무슨 일이 일어날지 여러분이 결정합니다. 이는 실제 결제 수단이 필요하거나 샌드박스 계정의 지연 및 복잡성을 처리할 필요 없이 몇 분 안에 모든 시나리오(성공, 실패 및 취소)를 철저히 테스트할 수 있음을 의미합니다. Google Play와 통합할 준비가 되면 구매 처리 로직이 견고하다는 확신을 가질 수 있습니다.

test-store-testing

자동화된 단위 테스트 작성하기

0:08:00

Test Store의 가장 강력한 기능 중 하나는 인앱 구매 로직의 자동화된 단위 테스트를 가능하게 하는 것입니다. CI/CD에서 안정적으로 실행되는 테스트를 작성해 보겠습니다.

1단계: 테스트 종속성 추가하기

build.gradle.kts에 다음을 추가하십시오:

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

    // Testing dependencies
    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단계: Purchase Repository 생성하기

먼저 테스트 가능한 purchase repository를 생성하겠습니다:

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단계: 비즈니스 로직이 포함된 View Model 생성하기

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 ?: "Unknown error")
            }
        }
    }

    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("Purchase completed but entitlement not active")
                }
            } 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() {
        // Initialize RevenueCat with Test Store API key
        Purchases.configure(
            PurchasesConfiguration.Builder(
                ApplicationProvider.getApplicationContext(),
                "test_YOUR_KEY_HERE"
            ).build()
        )

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

    @Test
    fun testLoadProducts_Success() = runTest {
        // Given: ViewModel is initialized

        // When: Loading products
        viewModel.loadProducts()

        // Wait for state to update
        advanceUntilIdle()

        // Then: Products should be loaded successfully
        val state = viewModel.state.value
        assertTrue(state is PaywallState.Success)
        assertFalse((state as PaywallState.Success).products.isEmpty())
    }

    @Test
    fun testPurchaseProduct_Success() = runTest {
        // Given: Products are loaded
        viewModel.loadProducts()
        advanceUntilIdle()

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

        // When: User purchases product and Test Store dialog shows "Successful Purchase"
        // Note: In actual test, you'll need to interact with Test Store dialog
        viewModel.purchaseProduct(mockActivity, product)
        advanceUntilIdle()

        // Then: Purchase should succeed and entitlement should be active
        val finalState = viewModel.state.value
        assertTrue(finalState is PaywallState.PurchaseSuccess)
    }

    @Test
    fun testPurchaseProduct_Cancelled() = runTest {
        // Given: Products are loaded
        viewModel.loadProducts()
        advanceUntilIdle()

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

        // When: User cancels purchase in Test Store dialog
        viewModel.purchaseProduct(mockActivity, product)
        advanceUntilIdle()

        // Then: State should indicate cancellation
        val finalState = viewModel.state.value
        assertTrue(finalState is PaywallState.PurchaseCancelled)
    }
}

5단계: Mock 기반 테스트 대안

더 많은 제어를 위해 repository를 mock할 수 있습니다:

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: Mock successful purchase
        val mockProduct = mockk<StoreProduct>()
        val mockCustomerInfo = mockk<CustomerInfo> {
            every { entitlements["premium"]?.isActive } returns true
        }

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

        // When: Purchase is initiated
        viewModel.purchaseProduct(mockk(), mockProduct)
        advanceUntilIdle()

        // Then: State should be success
        assertTrue(viewModel.state.value is PaywallState.PurchaseSuccess)
    }

    @Test
    fun testPurchaseFailed() = runTest {
        // Given: Mock failed purchase
        val mockProduct = mockk<StoreProduct>()

        coEvery {
            mockRepository.purchaseProduct(any(), mockProduct)
        } throws PurchasesException(
            PurchasesErrorCode.PAYMENT_PENDING_ERROR,
            "Payment is pending"
        )

        // When: Purchase is initiated
        viewModel.purchaseProduct(mockk(), mockProduct)
        advanceUntilIdle()

        // Then: State should show error
        val state = viewModel.state.value
        assertTrue(state is PaywallState.Error)
        assertTrue((state as PaywallState.Error).message.contains("pending"))
    }
}

이 접근 방식이 작동하는 이유

이 테스트 접근 방식은 인앱 구매 테스트의 전통적인 문제점을 제거합니다. Test Store에는 네트워크 종속성이 없기 때문에 테스트가 매번 안정적으로 실행됩니다. 더 이상 연결 문제로 인한 불안정한 실패가 없습니다. 테스트는 몇 분이 아닌 몇 초 만에 실행되어 개발 중에 빠른 피드백을 제공합니다. 실제 결제 시스템으로는 재현하기 어렵거나 시간이 많이 걸리는 엣지 케이스를 포함하여 모든 시나리오에 걸쳐 완전한 테스트 커버리지를 달성할 수 있습니다.

우리가 구축한 아키텍처(비즈니스 로직을 SDK에서 분리하는 repository 패턴 사용)는 이러한 테스트를 빠르고 유지 관리하기 쉽게 만듭니다. GitHub Actions와 같은 모든 CI/CD 환경에서 실행할 수 있어 프로덕션에 도달하기 전에 버그를 포착할 수 있습니다.

CI/CD에서 테스트 실행하기

0:04:00

이제 GitHub Actions에서 테스트가 자동으로 실행되도록 설정하여 모든 코드 변경 시 구매 로직이 안정적으로 유지되도록 하겠습니다.

1단계: GitHub Actions Workflow 생성하기

.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단계: GitHub Secrets에 Test Store API 키 추가하기

  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단계: Secret을 사용하도록 Gradle 설정하기

환경 변수를 사용하도록 build.gradle.kts를 업데이트하십시오:

kotlin
android {
    defaultConfig {
        // Get Test Store API key from environment or use placeholder
        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/

설정 확인하기

변경 사항을 커밋하고 푸시하여 workflow를 트리거하십시오. 리포지토리의 GitHub Actions 탭으로 이동하여 테스트가 자동으로 실행되는 것을 확인하십시오. workflow가 코드를 체크아웃하고, 환경을 설정하고, 테스트 스위트를 실행하는 동안 실시간 진행 상황을 볼 수 있습니다. 몇 분 안에 CI 환경에서 모든 테스트가 통과하는지 알 수 있습니다.

자동화된 테스트의 강력함

일단 설정되면 모든 pull request가 병합되기 전에 자동으로 검증됩니다. 변경 사항이 구매 로직을 손상시키면 즉시 피드백을 받게 되며 모든 시나리오를 수동으로 테스트할 필요가 없습니다. Test Store는 모든 실행에서 테스트가 일관된 결과를 생성하도록 보장하여 불안정한 테스트의 좌절감을 제거합니다. 이는 버그가 있는 코드가 main 브랜치에 도달하는 것을 방지하는 품질 게이트를 만들고 반복적인 수동 테스트에 소비될 수많은 시간을 절약합니다.

모범 사례 및 프로덕션 설정

0:03:00

Test Store를 설정했으므로 이제 모범 사례와 Test Store와 프로덕션 환경 간 전환 방법을 다루겠습니다.

환경 분리

항상 Test Store와 프로덕션 키를 분리하여 유지하십시오:

kotlin
object RevenueCatKeys {
    // Test Store - for testing only
    const val TEST_STORE_KEY = "test_xxxxx"

    // Google Play - for production
    const val GOOGLE_PLAY_KEY = "goog_xxxxx"

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

Build Variant 설정

build variants를 사용하여 환경 간 자동 전환을 구현하십시오:

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
// ❌ BAD: Hardcoded keys
val apiKey = "test_xxxxx"

// ✅ GOOD: Environment-based configuration
val apiKey = if (BuildConfig.DEBUG) {
    BuildConfig.TEST_STORE_API_KEY
} else {
    BuildConfig.GOOGLE_PLAY_API_KEY
}

// ✅ BETTER: Check for test environment
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", "Using Test Store for testing")
} else if (BuildConfig.DEBUG) {
    Purchases.logLevel = LogLevel.DEBUG
} else {
    Purchases.logLevel = LogLevel.INFO
}

프로덕션으로 전환하기

앱을 출시하기 전에 철저한 체크리스트를 완료했는지 확인하십시오. 모든 테스트가 Test Store와 함께 통과해야 하며 Google Play Sandbox와의 통합을 수동으로 확인해야 합니다. build variants가 각 환경에서 올바른 API 키를 사용하도록 올바르게 설정되어 있는지 다시 확인하십시오. 프로덕션 API 키는 보안이 유지되어야 하며 버전 관리에 커밋되어서는 안 됩니다. Test Store 키가 릴리스 빌드에서 완전히 제거되었는지 확인하십시오. 디버그 구성에만 존재해야 합니다. 로깅이 적절하게 설정되어 있는지 확인하십시오(디버그에서는 상세, 프로덕션에서는 최소). 모든 시나리오에 걸쳐 오류 처리가 테스트되었는지 확인하십시오.

결론

축하합니다! Android용 RevenueCat의 Test Store를 성공적으로 설정했습니다. 이 codelab을 통해 RevenueCat 대시보드에서 Test Store를 활성화하고, Android 애플리케이션에서 Test Store API 키를 설정하고, 결정론적 결과로 구매 플로우를 테스트하고, 구매 로직에 대한 자동화된 단위 테스트를 작성하고, GitHub Actions와 같은 CI/CD 환경에서 해당 테스트를 실행하고, 환경 분리를 위한 모범 사례를 따르는 방법을 배웠습니다.

핵심 요점

Test Store는 인앱 구매 테스트에 접근하는 방식을 근본적으로 변화시킵니다. 샌드박스 계정과 씨름하고 불안정한 네트워크 종속 테스트를 다루던 시대는 지났습니다. 구매 결과에 대한 결정론적 제어를 통해 구현에 대한 진정한 확신을 주는 신뢰할 수 있는 테스트를 작성할 수 있습니다. CI/CD 통합은 모든 코드 변경 시 구매 로직이 지속적으로 검증됨을 의미하며, 몇 시간이 아닌 몇 초 만에 구매 플로우를 반복할 수 있습니다. 가장 중요한 것은 구매 관련 버그가 프로덕션에 도달하기 전에 포착하여 매출과 사용자 경험을 모두 보호할 수 있다는 것입니다.

다음 단계

이제 Test Store를 설정했으므로 이 기반 위에 구축할 시간입니다. 모든 구매 시나리오를 포괄하는 포괄적인 테스트 작성부터 시작하십시오. 해피 패스만 테스트하지 마십시오. 이러한 테스트를 CI/CD 파이프라인에 통합하여 모든 pull request에서 자동으로 실행되도록 하십시오. 다양한 실패 모드로 오류 처리를 철저히 테스트하여 앱이 정상적으로 저하되는지 확인하십시오. 준비가 되면 플랫폼별 테스트를 위해 Google Play Sandbox로 전환하여 구독 갱신과 같은 사항을 확인하십시오. 마지막으로 구매 로직이 모든 단계에서 철저히 검증되었음을 알고 자신 있게 출시하십시오.

추가 리소스

RevenueCat Test Store 블로그 게시물 RevenueCat Android SDK 문서 테스트 가이드 GitHub: Cat Paywall Compose

즐거운 테스트 되세요!