How to Test In-App Subscriptions on iOS and Android
The payment dialog vanishes when test agents run your app outside an official store environment. Here is the architecture that makes in-app subscription testing work in CI.
- Why the payment dialog disappears when test agents run your app outside an official store environment
- Why license testers and sandbox accounts fail at scale despite being the official recommendation
- The two-layer architecture: a real-store smoke test plus a programmable entitlement control plane
- Environment prerequisites for both Android and iOS before running the smoke test
- How Pie scripts any subscription state using
#{script_name}syntax - Why idempotent reset and per-worker user isolation are required, not optional
- How to keep your test API from ever reaching production
Why Subscription Testing Breaks in Automation
The Google Play Billing dialog. The StoreKit payment sheet. The confirmation that appears when a user taps Subscribe.
My team has talked to dozens of mobile engineering teams at Pie. Subscription testing comes up in almost every conversation. Teams want to test it. But when a test agent runs the app outside an official store environment, that dialog never appears. The tap does nothing. The payment sheet stays blank. The test fails, but not because the subscription is broken. It fails because the environment cannot reach the real store.
No testing tool can override this. It is how Google Play and Apple’s App Store work by design.
Most teams discover this late. A 7-day trial button ships with a UI bug that took manual QA hours to catch. A billing-retry banner never gets tested because reaching that state requires waiting for an actual subscription to lapse. I have watched teams accept this as a permanent gap in their mobile testing automation and move on.
There is a better architecture. It requires understanding exactly why the standard approaches fail.
The Store Environment Problem
When a real user downloads an app from the Play Store or App Store, the store’s payment infrastructure is wired into that specific installation. The app recognizes it came from an official channel, and the billing SDK activates accordingly.
When an automated test agent installs an app, it typically sideloads it: installing the APK or IPA directly without going through the official store. In a standard sideloaded build, the Google Play Billing dialog and Apple’s StoreKit payment sheet do not appear. The billing SDK has no store connection to open. The UI just sits there.
Sideloading alone is not always the culprit. On Android, license testers can work with sideloaded or debug builds, but only when the build’s package name matches the app registered in Play Console and the tester account is properly configured. Most CI environments do not meet these conditions. The root issue is running the app in a store-incompatible or unauthorized environment, not sideloading itself.
What this means in practice: Appium, Detox, Maestro, XCUITest, and every other framework that installs apps outside the official store channel will hit this wall unless the environment is carefully staged. We saw this pattern firsthand when working with subscription-based iOS and Android apps. The subscription button. The failed test. The same “it worked in manual testing” explanation, every time.
What the Industry Does Instead
Every major player in this space says the same thing when pressed. The problem is real, widely acknowledged, and largely unsolved.
The SDKs built specifically for subscription testing on iOS and Android have published their own guides on this. The most honest conclusion in any of them is also the most deflating. Automated subscription testing is doable, but very difficult and limited. That is not a third-party critique. That is an SDK vendor describing the limits of its own tooling. Apple’s sandbox made it worse in late 2024 when renewal intervals stretched from minutes to once every 24 hours per cycle. Verifying a full subscription lifecycle in CI now means waiting a full day between state changes.
Apple and Google offer no programmatic control over subscription state in test environments. No API to put a user into billing retry. No way to jump to trial day 6. No clean reset between runs. Whatever state a test leaves behind, the next test inherits.
Search YouTube, Stack Overflow, and engineering blogs for “automated subscription testing CI.” Every result points the same direction. Use sandbox accounts manually. Accept the limitations. Skip the hard edge cases.
My team stopped accepting that answer.
How does Pie handle subscription testing?
See how the programmable entitlement control plane works and what it takes to set it up for your app.
Why Sandbox Accounts Do Not Scale
The standard advice is to use license testers. Google Play Console lets you designate accounts that can go through purchase flows without being charged real money. Apple’s sandbox provides a similar mechanism. For one engineer running one test manually, this works.
For end-to-end testing at scale in CI, it falls apart immediately.
- Parallel runs need many accounts. Each concurrent worker needs its own Google or Apple test account. A test suite with 20 parallel workers needs 20 separate license tester accounts, all configured and maintained.
- Workers are ephemeral. Modern CI/CD infrastructure spins up fresh workers for each run and tears them down when done. Persistent accounts do not survive worker restarts.
- State leaks between tests. There is no programmatic way to reset a license tester’s subscription status between runs. If run N puts a user into billing retry, run N+1 starts with that same state. Test isolation breaks entirely.
- The sandbox clock is not yours. Apple’s sandbox accelerates renewals, but “5 minutes” is still real time you must wait. You cannot jump to trial day 6 on demand. You cannot trigger a refund event and immediately verify the UI response.
- External availability. When Apple’s sandbox goes down, every team’s subscription tests fail simultaneously. You wait for Apple to fix it.
| Approach | Parallel runs | State reset | Time-travel states | External dependency |
|---|---|---|---|---|
| License testers | One account per worker | Manual | No | Yes (Apple/Google) |
| Sandbox environment | Limited | No | No | Yes (Apple/Google) |
| Programmable entitlement plane | Unlimited | Programmatic | Yes | No |
Every sandbox approach shares the same root problem. You depend on infrastructure you do not control, with state you cannot reset, on timing you cannot determine. That is not a testing strategy. It is a permanent flakiness tax on your most important flows, producing the same symptoms as flaky tests from any other source.
Two Jobs, Two Different Approaches
Most teams treat “subscription testing” as one thing. It is not. There are two distinct jobs that require completely different approaches.
Job 1: Verify the payment sheet appears. Install the app from the Play Store or TestFlight. Open the paywall. Tap Subscribe. Confirm the native payment dialog appears. Cancel. Done.
Run it before a release. Do not build your entire subscription test suite on it.
Job 2: Verify the app behaves correctly across subscription states. Ninety percent of what subscription testing actually requires lives here. Premium features unlocking when they should. Paywalls appearing when they should. Trial countdown banners showing the right message on day 6. Billing-retry prompts appearing when a payment fails.
None of these states require a real purchase to test. They require setting the right preconditions and reading the right app behavior afterward.
Use Apple and Google billing for smoke tests. Use your app’s entitlement control plane for real E2E coverage.
Layer One: The Smoke Test
For Job 1, keep the real-store test small and intentional.
The test: install the app from the Play Store or TestFlight, navigate to the paywall, tap the subscription CTA, verify the native payment dialog appears, and cancel without completing the purchase.
A confidence check, not a regression suite. It confirms the app has a valid store configuration, the payment SDK is initialized, and the paywall can reach the store. Run it before every release. Do not run it for every subscription scenario.
When Pie runs this test, we are looking for one thing in the UI. The payment sheet appears with the correct product name and price. If it does not, the store configuration changed. The subscription logic is fine.
Before You Run the Smoke Test
Most CI failures in the smoke test layer are not failures at all. They are environment misconfigurations dressed as test errors. Before any test runs, these conditions need to be in place.
Android prerequisites:
- Build package name must match the app registered in Play Console exactly
- The test Google account must be added as a license tester under Play Console → Setup → License testing
- The app must be published to at least an internal track in Play Console (a draft is sufficient)
- The test device must be signed into the license tester account in device Settings
- Pie classifies environments that fail these conditions as
unsupported_environment, not a test failure
iOS prerequisites:
- App must be distributed via TestFlight, not a direct IPA install
- Tester must use an Apple sandbox account created in App Store Connect → Users and Access → Sandbox Testers
- The sandbox account must be signed in on the test device under Settings → App Store
- In-app purchase products must be in “Ready to Submit” status or higher in App Store Connect
Meet these conditions and the smoke test is reliable. Skip any one of them and you will get inconsistent results that look like a testing problem but are really an environment problem.
Layer Two: The Programmable Entitlement Control Plane
Job 2 requires direct, deterministic control over subscription state. The real store cannot give you that.
Your engineering team builds a test-only entitlement control plane. Protected backend endpoints that mirror the paths your production system takes via receipt validation and store webhooks. In production, a real billing event sets entitlement state. In a test build (activated by an environment variable or build flag), your test does.
No mocking, no store interception. You are talking directly to the entitlement layer your app already depends on, using the same paths a real store event would trigger.
Here is what the minimum contract looks like:
subscriptionTesting:
version: 1
states:
- free
- trialing
- premium
- expired
- billing_retry
- grace_period
- refunded
actions:
- resetUser
- setTier
- setSubState
- setTrialDay
- triggerRenewal
- triggerRefund
getters:
- getSubscription
- getEntitlements
Your backend exposes corresponding endpoints in a test-only namespace. The exact paths are yours to define. Pie registers whatever protected endpoints your app exposes and wraps them as callable scripts. A common pattern looks like this:
POST /test/subscription/reset
POST /test/subscription/set-tier
POST /test/subscription/set-state
POST /test/subscription/set-trial-day
POST /test/subscription/trigger-renewal
POST /test/subscription/trigger-refund
GET /test/subscription
The logic mirrors how engineering teams already test other backend-dependent mobile flows. Consider loan approval in a fintech app: you cannot approve a loan through the UI in a test environment. The QA agent calls a backend endpoint, tells it to approve the loan for this user, and then verifies the UI reflects the change when the app opens. Subscription testing works identically. The control plane does what a real store event would do. Because it routes through the same entitlement service, you are also testing your webhook handler and database update logic along the way.
Every subscription state your backend can express is now reachable on demand. Set tier, set trial day, trigger a refund. All in the same run. A universal reset clears state between test cases without touching the store. Billing events that used to take days to reproduce manually now take one script call.
Isolation and Reset Requirements
These are not optional. They are the conditions CI reliability depends on.
-
Idempotent reset before every test. Every test case must start with
#{reset_user}as its first step. If a previous test left the user in a billing-retry state, the next test that skips the reset inherits that state. One flaky run in a hundred becomes the cause of the next failure. The reset is cheap. The debugging time it prevents is not. -
Unique test user per parallel worker. When 20 workers run simultaneously and share a single test user, their state mutations collide. Worker A puts the user into
expired. Worker B simultaneously sets it topremium. Both tests read incorrect state and produce unreliable results. Each parallel worker needs its own isolated user identity so subscription state belongs exclusively to that run. No shared state, no collisions, no test flakiness from infrastructure your tests didn’t cause.
Reset before every test. One user per worker. Everything else follows from these.
Keeping the Test API Out of Production
A control plane that can flip any user to premium is also a critical vulnerability if it escapes into production. Four gates keep it contained.
-
Build flag gate. Compile test endpoints only into test builds. Use a build variant on Android or a scheme on iOS. If
TEST_SUBSCRIPTION_API_ENABLEDis not set, the routes do not exist. -
Environment variable check at runtime. Add a second gate at the handler level. If the environment variable is not present, return
404. Defense in depth: even if the code ships accidentally, the endpoint does not respond. -
Separate namespace with access control. All test endpoints live under a
/test/prefix (or equivalent). In production infrastructure, this prefix routes to a 403 or does not resolve. Apply a service token or internal API key so the endpoints are not open even in staging. -
Never expose subscription state mutation publicly. An endpoint that can set any user’s subscription tier is a business-critical attack surface. Treat it like a database admin endpoint: never accessible from the public internet.
How Pie Runs Subscription Tests
When a customer exposes these endpoints, Pie registers each one as a custom script the test agent can call directly. Script names use underscores and map to the operations your control plane exposes:
#{reset_user}
#{set_tier_premium}
#{set_sub_state_expired}
#{set_trial_day_6}
#{trigger_refund}
#{get_subscription}
A test case for verifying premium feature access:
#{reset_user}
#{set_tier_premium}
[open app]
[navigate to premium feature]
[verify feature is accessible]
A test case for verifying trial-ending messaging:
#{reset_user}
#{set_trial_day_6}
[open app]
[verify trial countdown banner shows correct day]
[verify upgrade CTA appears]
A test case for refund downgrade behavior:
#{reset_user}
#{set_tier_premium}
#{trigger_refund}
[open app]
[verify paywall appears]
[verify premium content is inaccessible]
Each test starts with #{reset_user} so the next run starts clean. Workers run in parallel because each gets its own isolated user. No shared state. No state leaks. The same autonomous testing loop that handles every other app flow handles subscription flows the same way.
Subscription States You Can Finally Test
The entitlement control plane opens scenarios that were previously unreachable in mobile app testing:
Billing retry. When a payment fails, the app should show a billing-retry banner prompting the user to update their payment method. With the real store, triggering a payment failure requires a declined test card and a timing window. With the control plane, #{set_sub_state_billing_retry} sets it instantly.
Grace period. Apple and Google provide a short window after payment failure where the subscription stays active. Verifying your app correctly handles this window requires entering it, which takes real time in the sandbox and is not reliably reachable outside a properly staged store environment. The control plane gets you there with a single script call.
Trial day 6 of a 7-day trial. On day 6, users should see an upgrade prompt. Reaching this state in sandbox testing means waiting 6 real minutes in Apple’s accelerated sandbox (or 24 hours during one of Apple’s bad weeks). Call #{set_trial_day_6} from your test setup. Done.
Cancellation and re-subscription flows. Cancel, verify downgrade, re-subscribe, verify restoration. In the real store this sequence requires multiple interactions and timing assumptions. Against the control plane it is deterministic, fast, and repeatable.
Refund edge cases. What happens when a user completes a purchase and requests a refund the same day? With the real store, this requires a real refund and a processing delay. With the control plane, #{trigger_refund} fires the event and you verify the UI response immediately.
The control plane also makes it possible to run these tests against every build in your CI/CD pipeline. Subscription coverage becomes part of your standard regression suite, not a special case that requires manual setup and a cooperative sandbox.
Subscription State as Infrastructure
Every other critical app state is controllable in CI. Auth state. Network state. Feature flags. UI state. Engineering teams have solved all of these at the infrastructure level.
Subscription state is the last one standing.
The tooling industry has accepted this gap as permanent. RevenueCat’s guide says so directly. Apple’s sandbox makes it worse every year. At Pie, we built this architecture because we got tired of telling customers that their most revenue-critical flows were essentially unreachable in automation.
The two-layer approach is not complicated. One smoke test confirms the store sheet appears, staged correctly for Android or iOS. A programmable entitlement control plane handles everything else, with clean state on every run, isolated users per worker, and test endpoints that will never ship to production.
Your billing stays on Apple and Google. Your test environment finally belongs to you.
Ready to set up subscription coverage for your app?
Pie handles the test execution. Your team wires the control plane. We will walk you through what it takes.
Frequently Asked Questions
More from the Pie blog