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
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:
- Revenue protection – ensure every valid purchase attempt results in the correct entitlement and that no money is lost or double‑charged.
- 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.
- 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– delivers purchase outcomes or errors.purchases)
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
- Query SKU details –
querySkuDetailsAsyncreturns pricing, title, and description. - Launch purchase –
launchBillingFlowwith the appropriateSkuDetails. - Receive result –
onPurchasesUpdatedprovides either a successfulPurchaseor an errorBillingResult. - Acknowledge/consume – call
acknowledgePurchase(non‑consumable/subscription) orconsumeAsync(consumable) within three days. - Grant entitlement – unlock content, update server, or award counts, etc.
- 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
- Install the internal test build on a physical device (or emulator with Google Play).
- Log in with a license test account via the Play Store app.
- Open the app and navigate to the store screen.
- Select a consumable SKU (e.g., “100 Coins”).
- Tap Buy – observe the Play purchase sheet.
- Complete the purchase using the test account’s credentials.
- Verify:
- App receives
onPurchasesUpdatedwith a non‑nullPurchase. - Consumption call succeeds.
- UI updates (coin balance increments).
- Backend receives a consumption verification request (check logs).
- Repeat for non‑consumable and subscription SKUs, acknowledging where required.
- Inject errors: turn off Wi‑Fi before step 5, rotate device, spam the Buy button, etc., and confirm error handling paths.
- 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/checkout@v3
- name: Set up JDK
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: '17'
- name: Run unit tests
run: ./gradlew testDebugUnitTest
- name: Run instrumented tests on Firebase Test Lab
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