How to Test In-App Purchases on Android (Complete Guide)

In‑app purchases (IAP) are the revenue engine for many Android apps. A failure in the purchase flow can turn a paying user into a frustrated one, trigger chargebacks, or even violate Google Play polic

January 12, 2026 · 18 min read · How-To Guides

Why In‑App Purchase Testing Matters

In‑app purchases (IAP) are the revenue engine for many Android apps. A failure in the purchase flow can turn a paying user into a frustrated one, trigger chargebacks, or even violate Google Play policies. Unlike UI bugs that may only affect aesthetics, IAP defects hit the bottom line directly: a missed purchase, a duplicate charge, or an inaccessible consumable can lead to revenue loss, negative reviews, and potential account suspension.

Testing IAP therefore serves three concrete goals:

  1. Revenue protection – ensure every valid purchase attempt results in the correct entitlement and that no money is lost or double‑charged.
  2. Policy compliance – Google Play requires that purchases be handled through the Billing Library, that receipts are verified, and that subscriptions follow renewal rules. Violations can lead to app removal.
  3. User trust – users expect a smooth, predictable flow when they spend money. Any friction, especially around error handling or refunds, erodes confidence and reduces lifetime value.

Because the purchase flow touches networking, server‑side validation, asynchronous callbacks, and Google Play’s billing service, bugs often surface only under specific conditions: low‑bandwidth networks, rooted devices, or when a user rapidly taps the buy button. A thorough test strategy must therefore combine happy‑path validation with aggressive error‑path and edge‑case probing.

Android Billing Library Essentials

BillingClient Lifecycle

The entry point is BillingClient. You instantiate it once per Activity or Application, establish a connection, and then launch flows. Key callbacks:

Proper lifecycle management prevents leaked connections and ensures that pending purchases are not lost when the app goes to the background.

Product Types

TypeConsumable?Typical UseRenewal Logic
ConsumableYesIn‑game currency, extra livesManaged by app; must consume after grant
Non‑consumableNoPremium unlocks, ad removalPermanent entitlement
SubscriptionNo (auto‑renew)Content access, service tiersHandled by Play; app receives renewal/cancel notifications

Each product type requires a distinct handling path: consumables need a consumeAsync call after granting the item; non‑consumables only need acknowledgment; subscriptions require handling of PurchaseState, acknowledgePurchase, and processing of renewal/cancel tokens from the developer server.

Purchase Flow Steps

  1. Query SKU detailsquerySkuDetailsAsync returns pricing, title, and description.
  2. Launch purchaselaunchBillingFlow with the appropriate SkuDetails.
  3. Receive resultonPurchasesUpdated provides either a successful Purchase or an error BillingResult.
  4. Acknowledge/consume – call acknowledgePurchase (non‑consumable/subscription) or consumeAsync (consumable) within three days.
  5. Grant entitlement – unlock content, update server, or award counts, etc.
  6. Persist purchase token – store locally or send to backend for verification.

Missing any step can cause the purchase to remain in a “pending” state, leading to user‑visible errors or revenue leakage.

Test Matrix for In‑App Purchases

Below is a comprehensive matrix that covers the dimensions you should verify for each SKU type. Each cell indicates the expected outcome and the verification method.

Test CategorySub‑testExpected ResultVerification Method
Happy PathPurchase consumable, consume, grantPurchase succeeds, consumption returns BillingResponseCode.OK, entitlement grantedUI shows reward, backend records consumption
Purchase non‑consumable, acknowledgePurchase succeeds, acknowledgment returns OK, entitlement permanentUI shows unlocked feature, purchase persists after app restart
Purchase subscription, acknowledgePurchase succeeds, acknowledgment OK, subscription activeUI shows premium badge, server receives webhook for renewal
Error PathsNetwork loss during launchBillingResult with SERVICE_DISCONNECTED or NETWORK_ERRORDisable Wi‑Fi/mobile data before launch, verify error dialog
Invalid SKU (typo)BillingResult with ITEM_UNAVAILABLEPass a non‑existent product ID, check error handling
User cancels purchase flowBillingResult with USER_CANCELEDPress back button during Play purchase sheet, ensure app returns to prior state
Duplicate purchase (non‑consumable) before acknowledgmentSecond call returns ITEM_ALREADY_OWNEDBuy same SKU twice fast, verify app does not grant twice
Purchase exceeds user’s balance (gift card)BillingResult with DEVELOPER_ERROR (or specific error code)Use a test account with limited funds, attempt high‑priced purchase
Edge CasesRapid double‑tap on buy buttonOnly one purchase flow initiated, second tap ignored or shows “already in progress”Use MonkeyRunner or Espresso to send two click events <200 ms apart
Purchase while app in backgroundFlow still launches; result delivered to onPurchasesUpdated regardless of lifecyclePress Home during purchase sheet, verify callback still fires
Device clock tampering (subscription)Subscription start/end dates adjust correctly; renewal not granted if clock set backChange system time via adb shell date, observe purchase validation
Rooted device with Play Store patchedPurchase may be blocked or return SERVICE_UNAVAILABLETest on a known rooted device or use Magisk Hide to simulate
Low storage conditionPurchase flow may fail with ERROR due to inability to write purchase tokenFill device storage to <10 MB, attempt purchase
AccessibilityTalkBack navigationAll purchase UI elements are reachable and announced correctlyEnable TalkBack, navigate purchase sheet with swipe gestures
Font scaling (200%)Layout does not truncate price or button textSet font scale to 200% in Settings, verify readability
Color contrastButtons meet WCAG AA contrast ratioUse Accessibility Scanner or manual check
Security/PrivacyReceipt verification on serverServer validates purchase token with Google Play Developer API, rejects tampered tokensIntercept network with Charles Proxy, modify token, ensure server rejects
No client‑side price tamperingChanging price in local JSON does not affect amount chargedUse a debugging proxy to alter SkuDetails JSON, confirm Play charges original price
Data minimizationOnly purchase token and necessary metadata sent to backendReview logs, ensure no personal data (email, IMEI) transmitted with purchase
Handling of refundsApp revokes entitlement immediately after refund webhookSimulate refund via Play Console, verify entitlement removal

The matrix can be expanded per product variant (e.g., different subscription tiers) but the core categories remain the same.

Manual Testing Approach

Preparing Test Accounts

Google Play offers license test accounts that can make purchases without charging real money. Add the tester’s email to the License testing section in the Play Console, then upload the app to an internal test track. The account will see [sandbox] next to product prices and will receive a test transaction ID.

Using the Billing Library in Sandbox

When the app is installed from an internal test track, the Billing Library automatically routes calls to Google Play’s sandbox environment. No code changes are required; however, you must ensure the app’s versionCode matches the one uploaded to the test track.

Adb Commands for State Reset

Repeated manual testing benefits from a clean slate:


# Clear app data and cache (removes locally stored purchase tokens)
adb shell pm clear com.example.myapp

# Force stop the Billing Service to simulate a disconnect
adb shell am force-stop com.android.vending

# Query current purchases (requires the app to have the BIND_GET_INSTALL_REFERRER permission)
adb shell cmd package install-commit com.example.myapp

The clear command is especially useful before testing consumable flows to guarantee that each run starts with zero owned items.

Step‑by‑Step Manual Test Script

  1. Install the internal test build on a physical device (or emulator with Google Play).
  2. Log in with a license test account via the Play Store app.
  3. Open the app and navigate to the store screen.
  4. Select a consumable SKU (e.g., “100 Coins”).
  5. Tap Buy – observe the Play purchase sheet.
  6. Complete the purchase using the test account’s credentials.
  7. Verify:
  1. Repeat for non‑consumable and subscription SKUs, acknowledging where required.
  2. Inject errors: turn off Wi‑Fi before step 5, rotate device, spam the Buy button, etc., and confirm error handling paths.
  3. Accessibility check: enable TalkBack, repeat a purchase flow, listen for announcements.

Document each step’s outcome in a test‑run spreadsheet; any deviation flags a defect for follow‑up.

Automated Testing Approaches

Unit Tests with MockBillingClient

Google provides a fake implementation of BillingClient for unit tests. By injecting this mock, you can verify that your ViewModel or Repository correctly maps billing results to UI states without needing a device or network.


// PurchaseViewModelTest.kt
class PurchaseViewModelTest {

    private lateinit var mockBillingClient: MockBillingClient
    private lateinit var viewModel: PurchaseViewModel

    @Before
    fun setUp() {
        mockBillingClient = MockBillingClient()
        viewModel = PurchaseViewModel(mockBillingClient)
    }

    @Test
    fun `consumable purchase triggers consumption`() {
        // Simulate a successful purchase
        val purchase = Purchase(
            purchaseToken = "token123",
            sku = "coins_100",
            purchaseTime = System.currentTimeMillis(),
            purchaseState = PurchaseState.PURCHASED
        )
        mockBillingClient.setPurchaseResult(purchase)

        // Initiate purchase from UI layer
        viewModel.buyCoins()

        // Verify consumption was called
        assertTrue(mockBillingClient.consumeCalled)
        assertEquals("token123", mockBillingClient.lastConsumedToken)
    }
}

The MockBillingClient class (available in the com.android.billingclient:billing-test artifact) lets you preset querySkuDetailsAsync responses, return specific BillingResult codes, and simulate network failures by throwing exceptions.

Instrumented Tests with Espresso + WireMock

For end‑to‑end validation on a device or emulator, you can combine Espresso UI actions with a local mock server that mimics Google Play’s billing responses. WireMock lets you define JSON responses for skuDetails and purchase endpoints, while the actual network call is intercepted via OkHttp’s MockWebServer.


// PurchaseFlowTest.kt
@RunWith(AndroidJUnit4::class)
class PurchaseFlowTest {

    private lateinit var mockWebServer: MockWebServer

    @Before
    fun setUp() {
        mockWebServer = MockWebServer()
        mockWebServer.start()
        // Configure OkHttp client used by BillingLibrary to point to mock server
        BillingTestUtil.setOkHttpDispatcher(mockWebServer.dispatcher)
    }

    @After
    fun tearDown() {
        mockWebServer.shutdown()
    }

    @Test
    fun `subscription purchase acknowledges and grants entitlement`() {
        // Mock skuDetails response
        mockWebServer.enqueue(MockResponse()
            .setResponseCode(200)
            .setBody("""{
                "skuDetails": [{
                    "sku": "sub_monthly",
                    "price": "$4.99",
                    "price_amount_micros": 4990000,
                    "price_currency_code": "USD",
                    "title": "Monthly Premium",
                    "description": "Access for 30 days",
                    "type": "SUBS"
                }]
            }""".trimIndent()))

        // Mock purchase flow success
        mockWebServer.enqueue(MockResponse()
            .setResponseCode(200)
            .setBody("""{
               


## Why In‑App Purchase Testing Matters  

In‑app purchases (IAP) are the revenue engine for many Android apps. A failure in the purchase flow can turn a paying user into a frustrated one, trigger chargebacks, or even violate Google Play policies. Unlike UI bugs that may only affect aesthetics, IAP defects hit the bottom line directly: a missed purchase, a duplicate charge, or an inaccessible consumable can lead to revenue loss, negative reviews, and potential account suspension.  

Testing IAP therefore serves three concrete goals:  

1. **Revenue protection** – ensure every valid purchase attempt results in the correct entitlement and that no money is lost or double‑charged.  
2. **Policy compliance** – Google Play requires that purchases be handled through the Billing Library, that receipts are verified, and that subscriptions follow renewal rules. Violations can lead to app removal.  
3. **User trust** – users expect a smooth, predictable flow when they spend money. Any friction, especially around error handling or refunds, erodes confidence and reduces lifetime value.  

Because the purchase flow touches networking, server‑side validation, asynchronous callbacks, and Google Play’s billing service, bugs often surface only under specific conditions: low‑bandwidth networks, rooted devices, or when a user rapidly taps the buy button. A thorough test strategy must therefore combine happy‑path validation with aggressive error‑path and edge‑case probing.

## Android Billing Library Essentials  

### BillingClient Lifecycle  

The entry point is `BillingClient`. You instantiate it once per `Activity` or `Application`, establish a connection, and then launch flows. Key callbacks:  

- `onBillingServiceConnected()` – signals the client is ready.  
- `onBillingServiceDisconnected()` – triggers reconnection logic.  
- `onPurchasesUpdated(BillingResult result, List<Purchase> purchases)` – delivers purchase outcomes or errors.  

Proper lifecycle management prevents leaked connections and ensures that pending purchases are not lost when the app goes to the background.

### Product Types  

| Type          | Consumable? | Typical Use | Renewal Logic |
|---------------|-------------|-------------|---------------|
| Consumable    | Yes         | In‑game currency, extra lives | Managed by app; must consume after grant |
| Non‑consumable| No          | Premium unlocks, ad removal | Permanent entitlement |
| Subscription  | No (auto‑renew) | Content access, service tiers | Handled by Play; app receives renewal/cancel notifications |

Each product type requires a distinct handling path: consumables need a `consumeAsync` call after granting the item; non‑consumables only need acknowledgment; subscriptions require handling of `PurchaseState`, `acknowledgePurchase`, and processing of renewal/cancel tokens from the developer server.

### Purchase Flow Steps  

1. **Query SKU details** – `querySkuDetailsAsync` returns pricing, title, and description.  
2. **Launch purchase** – `launchBillingFlow` with the appropriate `SkuDetails`.  
3. **Receive result** – `onPurchasesUpdated` provides either a successful `Purchase` or an error `BillingResult`.  
4. **Acknowledge/consume** – call `acknowledgePurchase` (non‑consumable/subscription) or `consumeAsync` (consumable) within three days.  
5. **Grant entitlement** – unlock content, update server, or award counts.  
6. **Persist purchase token** – store locally or send to backend for verification.  

Missing any step can cause the purchase to remain in a “pending” state, leading to user‑visible errors or revenue leakage.

## Test Matrix for In‑App Purchases  

Below is a comprehensive matrix that covers the dimensions you should verify for each SKU type. Each cell indicates the expected outcome and the verification method.

| **Test Category** | **Sub‑test** | **Expected Result** | **Verification Method** |
|-------------------|--------------|---------------------|--------------------------|
| **Happy Path**    | Purchase consumable, consume, grant | Purchase succeeds, consumption returns `BillingResponseCode.OK`, entitlement granted | UI shows reward, backend records consumption |
|                   | Purchase non‑consumable, acknowledge | Purchase succeeds, acknowledgment returns OK, entitlement permanent | UI shows unlocked feature, purchase persists after app restart |
|                   | Purchase subscription, acknowledge | Purchase succeeds, acknowledgment OK, subscription active | UI shows premium badge, server receives webhook for renewal |
| **Error Paths**   | Network loss during launch | `BillingResult` with `SERVICE_DISCONNECTED` or `NETWORK_ERROR` | Disable Wi‑Fi/mobile data before launch, verify error dialog |
|                   | Invalid SKU (typo) | `BillingResult` with `ITEM_UNAVAILABLE` | Pass a non‑existent product ID, check error handling |
|                   | User cancels purchase flow | `BillingResult` with `USER_CANCELED` | Press back button during Play purchase sheet, ensure app returns to prior state |
|                   | Duplicate purchase (non‑consumable) before acknowledgment | Second call returns `ITEM_ALREADY_OWNED` | Buy same SKU twice fast, verify app does not grant twice |
|                   | Purchase exceeds user’s balance (gift card) | `BillingResult` with `DEVELOPER_ERROR` (or specific error code) | Use a test account with limited funds, attempt high‑priced purchase |
| **Edge Cases**    | Rapid double‑tap on buy button | Only one purchase flow initiated, second tap ignored or shows “already in progress” | Use MonkeyRunner or Espresso to send two click events <200 ms apart |
|                   | Purchase while app in background | Flow still launches; result delivered to `onPurchasesUpdated` regardless of lifecycle | Press Home during purchase sheet, verify callback still fires |
|                   | Device clock tampering (subscription) | Subscription start/end dates adjust correctly; renewal not granted if clock set back | Change system time via `adb shell date`, observe purchase validation |
|                   | Rooted device with Play Store patched | Purchase may be blocked or return `SERVICE_UNAVAILABLE` | Test on a known rooted device or use Magisk Hide to simulate |
|                   | Low storage condition | Purchase flow may fail with `ERROR` due to inability to write purchase token | Fill device storage to <10 MB, attempt purchase |
| **Accessibility** | TalkBack navigation | All purchase UI elements are reachable and announced correctly | Enable TalkBack, navigate purchase sheet with swipe gestures |
|                   | Font scaling (200%) | Layout does not truncate price or button text | Set font scale to 200% in Settings, verify readability |
|                   | Color contrast | Buttons meet WCAG AA contrast ratio | Use Accessibility Scanner or manual check |
| **Security/Privacy**| Receipt verification on server | Server validates purchase token with Google Play Developer API, rejects tampered tokens | Intercept network with Charles Proxy, modify token, ensure server rejects |
|                   | No client‑side price tampering | Changing price in local JSON does not affect amount charged | Use a debugging proxy to alter `SkuDetails` JSON, confirm Play charges original price |
|                   | Data minimization | Only purchase token and necessary metadata sent to backend | Review logs, ensure no personal data (email, IMEI) transmitted with purchase |
|                   | Handling of refunds | App revokes entitlement immediately after refund webhook | Simulate refund via Play Console, verify entitlement removal |

The matrix can be expanded per product variant (e.g., different subscription tiers) but the core categories remain the same.

## Manual Testing Approach  

### Preparing Test Accounts  

Google Play offers **license test accounts** that can make purchases without charging real money. Add the tester’s email to the **License testing** section in the Play Console, then upload the app to an internal test track. The account will see **[sandbox]** next to product prices and will receive a test transaction ID.  

### Using the Billing Library in Sandbox  

When the app is installed from an internal test track, the Billing Library automatically routes calls to Google Play’s sandbox environment. No code changes are required; however, you must ensure the app’s versionCode matches the one uploaded to the test track.  

### Adb Commands for State Reset  

Repeated manual testing benefits from a clean slate:  

# Clear app data and cache (removes locally stored purchase tokens)

adb shell pm clear com.example.myapp

# Force stop the Billing Service to simulate a disconnect

adb shell am force-stop com.android.vending

# Query current purchases (requires the app to have the BIND_GET_INSTALL_REFERRER permission)

adb shell cmd package install-commit com.example.myapp



The `clear` command is especially useful before testing consumable flows to guarantee that each run starts with zero owned items.

### Step‑by‑Step Manual Test Script  

1. **Install the internal test build** on a physical device (or emulator with Google Play).  
2. **Log in with a license test account** via the Play Store app.  
3. **Open the app** and navigate to the store screen.  
4. **Select a consumable SKU** (e.g., “100 Coins”).  
5. **Tap Buy** – observe the Play purchase sheet.  
6. **Complete the purchase** using the test account’s credentials.  
7. **Verify**:  
   - App receives `onPurchasesUpdated` with a non‑null `Purchase`.  
   - Consumption call succeeds.  
   - UI updates (coin balance increments).  
   - Backend receives a consumption verification request (check logs).  
8. **Repeat** for non‑consumable and subscription SKUs, acknowledging where required.  
9. **Inject errors**: turn off Wi‑Fi before step 5, rotate device, spam the Buy button, etc., and confirm error handling paths.  
10. **Accessibility check**: enable TalkBack, repeat a purchase flow, listen for announcements.  

Document each step’s outcome in a test‑run spreadsheet; any deviation flags a defect for follow‑up.

## Automated Testing Approaches  

### Unit Tests with MockBillingClient  

Google provides a fake implementation of `BillingClient` for unit tests. By injecting this mock, you can verify that your ViewModel or Repository correctly maps billing results to UI states without needing a device or network.  

// PurchaseViewModelTest.kt

class PurchaseViewModelTest {

private lateinit var mockBillingClient: MockBillingClient

private lateinit var viewModel: PurchaseViewModel

@Before

fun setUp() {

mockBillingClient = MockBillingClient()

viewModel = PurchaseViewModel(mockBillingClient)

}

@Test

fun consumable purchase triggers consumption() {

// Simulate a successful purchase

val purchase = Purchase(

purchaseToken = "token123",

sku = "coins_100",

purchaseTime = System.currentTimeMillis(),

purchaseState = PurchaseState.PURCHASED

)

mockBillingClient.setPurchaseResult(purchase)

// Initiate purchase from UI layer

viewModel.buyCoins()

// Verify consumption was called

assertTrue(mockBillingClient.consumeCalled)

assertEquals("token123", mockBillingClient.lastConsumedToken)

}

}



The `MockBillingClient` class (available in the `com.android.billingclient:billing-test` artifact) lets you preset `querySkuDetailsAsync` responses, return specific `BillingResult` codes, and simulate network failures by throwing exceptions.

### Instrumented Tests with Espresso + WireMock  

For end‑to‑end validation on a device or emulator, you can combine Espresso UI actions with a local mock server that mimics Google Play’s billing responses. WireMock lets you define JSON responses for `skuDetails` and `purchase` endpoints, while the actual network call is intercepted via OkHttp’s `MockWebServer`.  

// PurchaseFlowTest.kt

@RunWith(AndroidJUnit4::class)

class PurchaseFlowTest {

private lateinit var mockWebServer: MockWebServer

@Before

fun setUp() {

mockWebServer = MockWebServer()

mockWebServer.start()

// Configure OkHttp client used by BillingLibrary to point to mock server

BillingTestUtil.setOkHttpDispatcher(mockWebServer.dispatcher)

}

@After

fun tearDown() {

mockWebServer.shutdown()

}

@Test

fun subscription purchase acknowledges and grants entitlement() {

// Mock skuDetails response

mockWebServer.enqueue(MockResponse()

.setResponseCode(200)

.setBody("""{

"skuDetails": [{

"sku": "sub_monthly",

"price": "$4.99",

"price_amount_micros": 4990000,

"price_currency_code": "USD",

"title": "Monthly Premium",

"description": "Access for 30 days",

"type": "SUBS"

}]

}""".trimIndent()))

// Mock purchase flow success

mockWebServer.enqueue(MockResponse()

.setResponseCode(200)

.setBody("""{

"purchaseToken": "tok_abc123",

"purchaseState": 0

}""".trimIndent()))

// Launch the app and navigate to store screen

launchActivity()

onView(withId(R.id.btn_buy_subscription)).perform(click())

// Verify that acknowledgment was called

onView(withId(R.id.tv_subscription_status))

.check(matches(withText("Subscribed")))

// Verify backend received acknowledgment (via a fake endpoint)

mockWebServer.takeRequest().assertThat()

.hasPath("/acknowledge")

.hasBodyContains("tok_abc123")

}

}



This test validates that the UI triggers the purchase flow, that the mock server returns a successful purchase, and that the app proceeds to acknowledgment and UI update. By swapping the enqueued responses you can simulate error codes, network delays, or malformed payloads.

### Using SUSA for Autonomous Exploration  

SUSA (susatest.com) is an autonomous QA platform that explores an app without pre‑written scripts. After you upload an APK or point it at a web URL, SUSA launches a set of persona‑driven agents—curious, impatient, novice, adversarial, elderly, accessibility, power user, and others—each with its own behavior profile. The agents tap, scroll, type, handle dialogs, and attempt real flows, including in‑app purchase attempts, while monitoring for crashes, ANRs, dead buttons, WCAG violations, and security issues.  

Because SUSA does not rely on hard‑coded test cases, it can discover purchase‑flow bugs that scripted tests miss:  

- An **impatient** persona may double‑tap the buy button faster than a script’s wait time, exposing a race condition that grants duplicate consumables.  
- An **accessibility** persona using TalkBack may encounter a purchase button that is not correctly labeled, revealing a WCAG contrast or focus‑order issue that a sighted tester would not notice.  
- An **adversarial** persona may attempt to tamper with the purchase request via a rooted device or a proxy, surfacing insufficient server‑side validation.  

To run SUSA against your APK:  

pip install susatest-agent

susatest run --apk path/to/your/app.apk --personas all --output-dir ./susa-report



The generated report includes a flow trace for each persona, highlighting any purchase‑related failures (e.g., “Purchase flow abandoned after 3 seconds – possible dead button”). You can then feed those findings back into your manual or automated test suites.

### CI Integration  

Automated IAP tests should run on every pull request to catch regressions early. A typical GitHub Actions workflow might look like:  

name: IAP Verification

on:

pull_request:

branches: [ main ]

jobs:

billing-tests:

runs-on: ubuntu-latest

steps:

uses: actions/setup-java@v3

with:

distribution: temurin

java-version: '17'

run: ./gradlew testDebugUnitTest

uses: firebase/toolchain@v0

with:

command: test android run \

--type instrumentation \

--app app/build/outputs/apk/debug/app-debug.apk \

--test app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk \

--device model=Pixel3,version=30,locale=en,orientation=portrait



The unit test step validates business logic with `MockBillingClient`. The Firebase Test Lab step runs the Espresso+WireMock instrumented tests on a real device matrix, ensuring that the mocked server behaves correctly across different Android versions and screen densities.

## Tooling and Libraries  

| **Category**            | **Tool / Library**                                   | **Purpose**                                                                 |
|--------------------------|------------------------------------------------------|-----------------------------------------------------------------------------|
| Billing Implementation   | `com.android.billingclient:billing:6.2.1`            | Official Google Play Billing Library (latest stable)                        |
| Mocking for Unit Tests   | `com.android.billingclient:billing-test:6.2.1`       | Provides `MockBillingClient` for deterministic unit tests                  |
| Network Mocking          | `com.squareup.okhttp3:mockwebserver:4.12.0`          | Simulates Play server responses for instrumented tests                     |
| UI Automation            | `androidx.test.espresso:espresso-core:3.5.1`         | Drives UI interactions in instrumented tests                               |
| Accessibility Scanning   | `com.google.android.apps.accessibility:scanner:1.2`  | Detects WCAG violations on purchase screens                                 |
| Proxy / Traffic Inspection| Charles Proxy, mitmproxy                            | Intercepts and modifies HTTP/HTTPS to test tampering or network failures   |
| Device State Management  | `adb`                                                | Clears data, forces stops, simulates low storage, changes system time      |
| Autonomous Exploration   | `susatest-agent` (pip)                               | Runs persona‑driven exploration to surface unexpected IAP bugs             |
| CI / Test Farm           | Firebase Test Lab, AWS Device Farm                   | Executes instrumented tests on a matrix of real devices                    |

When selecting a version of the Billing Library, always check the release notes for breaking changes—especially between versions 5 and 6, where the `PurchaseFlowParams` API changed. Keep your `compileSdkVersion` and `targetSdkVersion` at least 33 to avoid runtime warnings on newer Android releases.

## Real‑World Production Gotchas  

Even after passing all manual and automated checks, certain issues only appear once the app is live. Understanding these helps you design better defensive code and monitoring.

### Network Flakiness  

Users often initiate purchases on spotty cellular connections. The Billing Library may return `SERVICE_DISCONNECTED` or `NETWORK_ERROR` after a timeout, but the Play server might still have processed the transaction. If your app treats any non‑OK result as a failure and does not query `queryPurchasesAsync` afterward, you risk **granting nothing** while the user is charged.  

**Defensive pattern:** after any purchase attempt (success or error), call `queryPurchasesAsync` to reconcile local state with the server. If a purchase token appears that you have not yet acknowledged, proceed

Test Your App Autonomously

Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.

Try SUSA Free