How to Write Test Cases for In-App Purchases (With Examples)
How to Write Test Cases for In-App Purchases (With Examples)
How to Write Test Cases for In-App Purchases (With Examples)
How to Write Test Cases for In-App Purchases (With Examples): Foundations
Test case anatomy
A test case for an in‑app purchase (IAP) must contain five immutable elements: a unique identifier, preconditions that set the device, account, and store state, a precise sequence of user actions, the expected observable outcome, and a post‑condition that restores the environment for the next test. The identifier should follow a naming convention that makes traceability to requirements trivial, e.g., IAP-POS-001 for a positive purchase flow, IAP-NEG-012 for a negative network‑failure scenario, and IAP-EDG-023 for an edge case involving rapid taps. Preconditions often include logging into a test sandbox account, clearing the app’s purchase cache, and configuring network throttling tools to simulate specific latencies. Steps must be expressed in imperative language that a tester or an automation script can follow without interpretation: “Tap the ‘Buy 100 coins’ button”, “Enter sandbox password ‘test123’”, “Wait for the receipt dialog”. Expected results are binary: either the purchase succeeds and the consumable balance increments by the advertised amount, or a specific error message appears and no financial transaction is recorded. Post‑conditions may involve restoring the original balance, signing out of the sandbox account, or deleting locally stored receipts to avoid cross‑test contamination.
Requirements traceability
Every IAP feature originates from a product requirement document (PRD) or a user story that describes the purchasable item, its type (consumable, non‑consumable, subscription), price tier, and any regional restrictions. Map each test case to the requirement ID it validates. For example, a requirement stating “Users in the EU must see prices in EUR and be able to purchase a monthly subscription” generates at least three test cases: one verifying price display, one verifying successful purchase, and one verifying that the subscription renews correctly after the first billing period. Maintain a traceability matrix in a spreadsheet or a test‑management tool where rows are requirement IDs and columns are test case IDs; a checkmark indicates coverage. This practice prevents gaps when requirements evolve and provides auditors with evidence that all purchase‑related risks have been examined.
Data and environment preparation
IAP testing hinges on reproducible sandbox environments. For Android, use the Google Play Billing Library’s test mode with static product IDs reserved by Google (android.test.purchased, android.test.canceled, etc.) or upload a closed‑track version of the app and configure license test accounts in the Play Console. For iOS, rely on StoreKit Configuration Files or App Store Connect sandbox users. Prior to each test run, clear the app’s persistent storage (adb shell pm clear com.example.app on Android, xcrun simctl uninstall booted com.example.app followed by reinstall on iOS) to eliminate cached receipts. Set the device locale and currency to match the test scenario using ADB shell commands (adb shell setprop persist.sys.language en; adb shell setprop persist.sys.country US) or Xcode’s scheme environment variables. If the app consumes remote configuration flags (e.g., feature toggles for promotional offers), serve those flags via a local mock server that can return different JSON payloads on demand. This isolation guarantees that a test’s outcome depends solely on the steps executed, not on hidden state.
How to Write Test Cases for In-App Purchases (With Examples): Positive Flow Coverage
Successful purchase flow
The core positive test validates that a user can complete a purchase from product selection to receipt confirmation. Preconditions: a valid sandbox user with no existing entitlement for the product, network set to full speed, and the app foregrounded. Steps: navigate to the store screen, locate the product cell, tap the buy button, confirm the system prompt, enter sandbox credentials if required, and wait for the success toast. Expected result: the app displays a success message, updates the UI to reflect the purchased entitlement (e.g., coin count increments, premium badge appears), and writes a valid receipt to the local storage that can be verified against the server. Post‑condition: optionally consume the consumable to return the balance to zero for the next iteration.
Consumable, non‑consumable, and subscription distinctions
Each product type demands a slightly different validation matrix. For consumables, after a successful purchase the test must invoke the consumption API and verify that the balance returns to zero. For non‑consumables, the test checks that a subsequent purchase attempt yields an “already owned” message and that the entitlement persists across app restarts. For subscriptions, the test validates three phases: initial purchase, renewal simulation (by advancing the sandbox clock or using StoreKit’s SKPaymentQueue.finishTransaction with a renewal flag), and expiration handling (ensuring the premium UI reverts to free tier after the simulated period ends). The test case IDs reflect these variations: IAP-POS-002-CON, IAP-POS-002-NON, IAP-POS-002-SUB.
Restore purchases
Restoring ensures that users can recover non‑consumable items and subscriptions on a new device or after reinstalling. Preconditions: the sandbox user has previously purchased a non‑consumable or an active subscription, and the app’s local receipt store has been cleared. Steps: sign in with the same sandbox account, navigate to the settings menu, tap “Restore Purchases”, and wait for the completion callback. Expected result: the app reinstates the entitlement without charging again, displays a confirmation message, and the UI reflects the restored item. Any failure to restore constitutes a critical defect because it directly impacts user trust and can lead to chargebacks.
Handling successful callbacks
Beyond UI changes, the test must verify that the app’s internal purchase listener receives the correct callback objects. Instrument the code with a test double or a mock listener that records the onPurchaseSuccess event, capturing the product ID, transaction identifier, and receipt payload. Assert that the received product ID matches the one requested, that the transaction identifier is non‑empty, and that the receipt passes the server‑side validation endpoint (often a stub in the test environment). This level of verification catches bugs where the UI updates optimistically but the backend never receives a valid receipt, a scenario that can slip past manual UI‑only checks.
How to Write Test Cases for In-App Purchases (With Examples): Negative and Edge Cases
Network failures
Network instability is the most frequent cause of purchase failures in the wild. Simulate three conditions: total loss (airplane mode), high latency (200 ms round‑trip with 30 % packet loss), and intermittent drop‑outs during the transaction flow. Preconditions: device connected to a Wi‑Fi network shaped by tc on Linux or Network Link Conditioner on macOS; sandbox user logged in. Steps: initiate a purchase, apply the network impairment at the moment the app sends the payment request, and maintain it until the timeout period expires. Expected result: the app displays a clear, localized error (“Unable to connect to the store. Please try again.”), does not deduct any virtual currency, and leaves the receipt store untouched. Post‑condition: restore normal connectivity and confirm that a subsequent purchase succeeds, proving the recovery path works.
Invalid product identifiers
A typo in the product ID or a mismatched sandbox configuration leads to an “item not available” response. Preconditions: ensure the app’s product list is fetched from the server; replace one valid ID with a known invalid string (e.g., com.example.app.nonexistent). Steps: attempt to purchase the mis‑identified product. Expected result: the app shows an error message that does not reveal internal IDs (for security reasons) and logs the failure for diagnostics. No financial transaction should be attempted; the billing library should return an error code before contacting the server.
Payment declined / sandbox failures
Sandbox environments allow forcing specific payment outcomes via special product IDs. For Google Play, use android.test.item_unavailable to simulate a declined card; for Apple, use the SKErrorPaymentInvalid error code by setting the sandbox account’s payment method to none. Preconditions: sandbox user with a invalid payment method configured. Steps: attempt a purchase. Expected result: the app receives a failure callback, presents a user‑friendly message (“Your payment method could not be verified. Please update your billing information.”), and does not grant the entitlement. This test guards against scenarios where the app incorrectly treats a declined payment as successful due to missing error‑handling logic.
Race conditions, rapid taps
Power users may double‑tap a buy button or navigate away while the transaction is in progress. Preconditions: device with normal network, logged‑in sandbox user. Steps: tap the buy button twice within 150 ms, or tap the button and immediately press the back button. Expected result: the app processes only a single transaction, shows a single success or error dialog, and does not create duplicate receipts. Internally, the purchase manager should set a flag (isPurchaseInProgress) that blocks subsequent attempts until the current transaction finishes. Validating this flag through instrumentation or by checking that the server receives only one request confirms the guardrail works.
Receipt validation failures
Even when the client receives a success callback, the server may reject the receipt due to tampering or clock skew. Preconditions: a valid purchase has been made; intercept the receipt before it is sent to the validation endpoint and modify a single byte (e.g., flip the least‑significant bit of the receipt data). Steps: send the corrupted receipt to the mock validation server. Expected result: the server returns a 400 Bad Request or a custom error code, the app treats the purchase as unverified, rolls back any granted entitlement, and notifies the user of a verification issue. This test ensures that the app never trusts a client‑side success signal alone.
Currency/region mismatches
Users traveling or using VPNs may see prices in a currency that does not match their storefront locale. Preconditions: set device locale to Japan (ja_JP) but configure the sandbox storefront to United States (US) via the Play StoreKit configuration. Steps: navigate to the store and observe the displayed price. Expected result: the price shown corresponds to the storefront’s currency (USD) and not the device locale, or the app displays a disclaimer that prices may vary. If the app incorrectly converts using the device locale, users could be over‑ or under‑charged, leading to compliance issues.
Age/gating restrictions
Certain items (e.g., loot boxes, premium content) are restricted to users above a specific age. Preconditions: a sandbox account with a birthdate set to 12 years old; the product is marked as 18+. Steps: attempt to purchase the restricted item. Expected result: the app blocks the purchase attempt before reaching the billing layer, shows an age‑appropriate message (“This item is not available for your age.”), and logs the attempt for parental‑control auditing. Failure to enforce age gates can trigger legal penalties under COPPA or GDPR‑Kids.
Promo/offer codes
Promotional codes can grant free entitlements or discounted prices. Preconditions: a valid promo code that grants a 100 % discount on a consumable pack; the app’s promotion service is wired to the backend. Steps: enter the code in the redemption screen, then attempt to purchase the associated product. Expected result: the price displayed reflects the discount (often free), the transaction completes without charging the sandbox account, and the entitlement is granted. Additionally, test an expired or invalid code to confirm the app shows an appropriate error and does not proceed to payment.
Manual vs Automated Approaches for IAP Testing
Manual exploratory checklist
Even with automation, manual exploration uncovers UX friction that scripts miss. A concise checklist for a tester includes: verifying that all purchase buttons have sufficient touch target size (≥48 dp), confirming that error messages are localized and do not expose raw server responses, checking that the loading spinner appears immediately after a tap and disappears only after a definitive response, ensuring that the app does not allow navigation away from the purchase flow while a transaction is in progress, and validating that the app restores state correctly after a background/foreground cycle during a pending transaction. Document any deviations in a bug report with steps, device model, OS version, and network condition.
Automated unit / integration tests
Unit tests isolate the purchase manager logic. Mock the billing client interface (IBillingClient on Android, SKPaymentQueue delegate on iOS) and inject predefined responses for success, failure, and deferred states. Verify that the manager updates the internal state machine correctly, calls the consumption API for consumables, and invokes the restoration flow when requested. Integration tests launch the app on an emulator or simulator with a mocked network layer (e.g., OkHttp’s MockWebServer for Android, NSURLProtocol subclass for iOS). They drive the UI via Espresso or XCUITest, asserting UI changes and checking that the mock server receives the expected request payloads. These tests run fast on CI and guard against regressions in the purchase orchestration code.
UI test frameworks (Espresso, XCUITest)
UI tests replicate the exact user journey. On Android, an Espresso test might look like:
@Test
fun purchaseConsumable_success() {
// Preconditions
grantRuntimePermission("android.permission.POST_NOTIFICATIONS")
val user = TestAccount.sandboxUser()
login(user)
// Action
onView(withId(R.id.buy_100_coins)).perform(click())
onView(withText("Buy")).perform(click())
// Enter sandbox credentials via UiDevice if needed
// Validation
onView(withId(R.id.coin_balance)).check(matches(withText("150")))
onView(withText(R.string.purchase_success)).check(matches(isDisplayed()))
}
On iOS with XCUITest:
func testPurchaseSubscription_success() {
let app = XCUIApplication()
app.launch()
app.buttons["Subscribe Monthly"].tap()
app.alerts["Confirm Purchase"].buttons["Buy"].tap()
// Enter sandbox credentials via XCUITest's monitor if needed
XCTAssertTrue(app.staticTexts["Premium Access"].exists)
XCTAssertTrue(app.staticTexts["Thank you for subscribing!"].exists)
}
These tests should be tagged (@MediumTest, @UITest) and executed on a device farm that can simulate varying network profiles.
Using SUSA for autonomous exploration
SUSA can complement scripted tests by autonomously exercising purchase flows without predefined scripts. After uploading an APK or pointing SUSA at a web‑enabled storefront, the agent generates a session that taps, scrolls, and attempts purchases using its built‑in user personas. The curious persona may try every product, the impatient persona may rapid‑tap buy buttons, and the adversarial persona may manipulate network conditions via the agent’s integrated throttling. SUSA records each attempt, logs whether a receipt was generated, and flags any deviation from the expected PASS/FAIL verdict based on heuristics (e.g., a purchase that succeeds but does not update the UI). The output includes a detailed trace that can be imported into a test‑management tool to augment manual test cases. Because SUA learns from prior runs, subsequent executions focus on unexplored screens and edge‑case triggers, steadily increasing coverage without additional test‑authoring effort.
Prioritization and Risk‑Based Selection
Risk matrix
Not all IAP scenarios carry equal weight. Construct a 2 × 2 matrix where the X‑axis is Likelihood (Low, Medium, High) and the Y‑axis is Impact (Low, Medium, High). Populate each cell with the test case IDs that fall into that quadrant. For example:
| Likelihood \ Impact | Low Impact | Medium Impact | High Impact |
|---|---|---|---|
| High Likelihood | IAP-POS-001, IAP-POS-002-CON | IAP-NEG-001 (network loss) | IAP-NEG-003 (payment declined) |
| Medium Likelihood | IAP-EDG-005 (locale mismatch) | IAP-NEG-002 (invalid product ID) | IAP-NEG-004 (receipt validation) |
| Low Likelihood | IAP-EDG-008 (promo code expiry) | IAP-EDG-009 (age gating) | IAP-EDG-010 (restore after device change) |
Tests in the High‑Likelihood / High‑Impact quadrant receive top priority for every release; those in the Low‑Likelihood / Low‑Impact quadrant can be run less frequently or relegated to nightly cycles.
Impact vs likelihood scoring
Assign numeric values (1 = Low, 2 = Medium, 3 = High) to both axes and compute a Risk Priority Number (RPN) = Likelihood × Impact. Sort test cases by descending RPN to generate an execution order. This quantitative method simplifies communication with product managers and justifies test‑suite sizing decisions.
Traceability to requirement IDs
Maintain a two‑way link: each requirement ID lists the test case IDs that verify it, and each test case ID lists the requirement(s) it covers. In a spreadsheet, this appears as:
| Requirement ID | Description | Test Case IDs |
|---|---|---|
| REQ-IAP-01 | Users can purchase consumable coins | IAP-POS-001, IAP-POS-002-CON, IAP-NEG-001 |
| REQ-IAP-02 | Subscription renews monthly without manual intervention | IAP-POS-002-SUB, IAP-NEG-006 (renewal failure) |
| REQ-IAP-03 | Price displayed matches storefront currency | IAP-EDG-005, IAP-EDG-006 (VPN scenario) |
When a requirement changes, the tester can instantly see which test cases need revision, addition, or deletion, preventing orphaned tests that no longer map to any spec.
Maintaining and Evolving IAP Test Suites
Versioning test data
IAP tests often depend on static product identifiers and price strings stored in JSON or XML files. Treat these files as version‑controlled artifacts. When a new product is added, create a new version of the data file (e.g., iap_products_v2.json) and update the test suite to reference the version via an environment variable (IAP_DATA_VERSION=v2). This approach allows running tests against multiple product catalogs simultaneously—useful for verifying backward compatibility when a legacy product is deprecated but still present in older app builds.
Flaky test mitigation
Flakiness in IAP tests commonly stems from timing assumptions or residual state. Mitigate by:
- Making each test fully self‑contained: clear caches, sign out, and reset network conditions at the start.
- Using explicit waits tied to observable UI changes rather than fixed
sleepstatements. - Idempotent actions: if a test detects that an entitlement already exists (e.g., via a local flag), it skips the purchase step and proceeds to validation.
- Retrying only on known transient errors (e.g., network timeout) with a capped retry count and exponential backoff.
- Logging the exact timestamps of each step; analyze logs to detect patterns that indicate a specific race condition.
Regression script generation from autonomous runs
SUSA’s cross‑session learning captures the sequences it explores and the outcomes it observes. After a run, the platform‑specific scripts can be auto‑generated: for Android, an Appium Java test that replays the tapped coordinates, scrolls, and text entry; for Web, a Playwright TypeScript script that mirrors the navigations and button clicks. These scripts serve as a safety net: if a future code change breaks a flow discovered by SUSA, the generated regression test will fail instantly, alerting the team before the change reaches production. Because the scripts are derived from actual exploratory behavior, they often cover paths that a manual test author might overlook, such as a sequence where a user opens a promotional banner, navigates to the store, and then cancels a purchase mid‑flow.
Checklist for IAP Test Case Authors
- [ ] Assign a unique, requirement‑traceable ID (e.g.,
IAP-POS-001). - [ ] Document precise preconditions: account state, network profile, locale, app version.
- [ ] Write steps in imperative voice, avoiding ambiguous terms like “maybe” or “if possible”.
- [ ] Define a single, observable expected result (UI change, API call, state transition).
- [ ] Include a post‑condition that restores the environment for the next test.
- [ ] Tag the test with its type: Positive, Negative, Edge, Performance, Security.
- [ ] Link to the requirement ID(s) it validates.
- [ ] Specify the tools or frameworks used for automation (Espresso, XCUITest, MockWebServer, etc.).
- [ ] Note any special data needed (sandbox credentials, test product IDs, promo codes).
- [ ] Indicate priority based on the risk matrix (High/Medium/Low).
- [ ] Review for clarity: a colleague should be able to execute the test without asking questions.
Closing Takeaways
Writing effective test cases for in‑app purchases demands a disciplined blend of specification‑driven design, rigorous negative‑and‑edge‑case thinking, and pragmatic automation. Start by mapping each purchasable item to a requirement, then derive a matrix of positive, negative, and edge scenarios that cover product types, payment outcomes, network conditions, and regulatory constraints. Use concrete, repeatable steps and unambiguous expected results so that manual testers and automation scripts can achieve identical outcomes. Prioritize efforts with a risk‑based matrix that weights likelihood and impact, ensuring that the most consequential failures are caught early. Complement scripted tests with autonomous explorers like SUSA to surface hidden paths and to generate regression suites that evolve with the application. Maintain test data versionally, eliminate flakiness through isolation and explicit waits, and keep traceability alive so that any change in requirements instantly reveals the corresponding test‑suite impact. By following this approach, you transform IAP testing from a ad‑hoc checklist into a measurable, scalable engineering practice that safeguards revenue, protects user trust, and keeps the storefront compliant across every release.
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