You can test most subscription states without a real purchase by combining three tools: a StoreKit configuration file in Xcode for fast local iteration on iOS, the App Store sandbox and Google Play license testers for store-level behavior, and seeded backend state for everything your server does after a purchase. Local tools cover the logic you own. Sandbox environments cover the store’s real flow. A small set of behaviors can only be confirmed in a production-like setup, and you should know which ones before you ship.
What states do you actually need to test?
A subscription is a state machine, and the bugs live in the transitions. Make a list before you pick tools. A practical minimum:
- Never subscribed.
- In a free trial or introductory offer.
- Active and renewing.
- Active but set to cancel at period end.
- In a billing retry or grace period after a failed charge.
- Expired.
- Refunded or revoked.
- Upgraded, downgraded or crossed over to a different plan.
- Restored on a new device.
For each state, write down two things: what the app should show, and what the backend should believe. Most subscription bugs are disagreements between those two. An app that shows “Premium” while the server returns a 403, or the reverse, is a state-sync bug, and you only find it by driving both sides through the same states.
How do StoreKit configuration files help on iOS?
A StoreKit configuration file is a local file in your Xcode project that defines products, subscription groups, prices, durations and offers. When you run the app with that file selected in the scheme, StoreKit serves those products from the local definition instead of contacting the App Store. No Apple ID, no network and no App Store Connect setup are needed to start.
The useful part is the Transaction Manager in Xcode. While the app runs, you can open it and act on test transactions:
- Approve or decline a pending purchase.
- Expire a subscription.
- Refund a transaction.
- Enable failure cases such as a billing problem or a purchase error.
The local environment also lets you speed up time. Subscription durations are real in production (a month is a month), but local testing can compress renewal rates so a monthly subscription renews in minutes. You can then watch several renewal cycles in one sitting and confirm your entitlement logic survives them.
The exact menu names and options change between Xcode versions, so check Apple’s guide to setting up StoreKit testing in Xcode for your version.
StoreKit testing also works from automated tests. Apple provides a test session type that lets unit and UI tests create transactions, force renewals, advance state and assert the result. If your entitlement logic is a function from transactions to access, test it directly with fixtures for each state in the list above. Pure functions are cheap to test and the cases are easy to enumerate.
// Pure entitlement logic is easy to test with fixtures for each state.
func entitlement(for transactions: [TransactionSnapshot], now: Date) -> Entitlement {
guard let latest = transactions.max(by: { $0.purchaseDate < $1.purchaseDate }) else {
return .none
}
if latest.revocationDate != nil { return .none }
if let expires = latest.expirationDate, expires <= now { return .expired }
return .active(until: latest.expirationDate)
}
The limit of local StoreKit testing is that the transactions are not signed by Apple’s production servers. They are signed with a local test key, so a backend that validates Apple’s signatures will correctly reject them unless you configure a test mode. That is a feature, not a flaw: it keeps fake receipts out of production logic.
What does the App Store sandbox add?
The sandbox is Apple’s test environment for the real purchase flow. You create sandbox test accounts in App Store Connect, sign in with one on a device, and make purchases that go through Apple’s sandbox servers. Nothing is charged.
Use the sandbox for what a local file cannot prove:
- Your products are configured correctly in App Store Connect, with the right IDs and groups.
- The purchase sheet, receipts and transactions reach your app as expected.
- Server notifications from Apple reach your backend, signed the way the store signs them.
- Your server’s verification of transactions with Apple works end to end.
Sandbox renewal timing is accelerated, and subscriptions renew a limited number of times before they expire on their own. This is useful for testing expiry, but it means a sandbox account is not a long-lived fixture. Expect to create fresh accounts or reset purchase history as you test. Apple documents the current renewal schedule, so read the sandbox testing guide rather than relying on a remembered number.
Sandbox behavior is also less deterministic than a local file. It depends on network calls, Apple’s test servers and the account’s history. Use it as a smoke test for the full path, not as your main regression suite.
How do you test Google Play subscriptions?
On Android, the equivalents are license testers and test payment methods. In the Play Console you add the Google accounts of your testers as license testers. Purchases made by those accounts go through the real Play Billing flow, but they are not charged. Play also offers test payment methods that simulate outcomes such as an approved payment or a declined one, which lets you exercise failure paths like a card that is declined at renewal.
Test subscriptions on Play renew on an accelerated schedule as well. Your app generally needs to be uploaded to a testing track for the billing flow to work, since Play Billing needs a build known to the Play Console. Check the Google Play Billing testing guide for current testing details.
Whatever the platform, the app is not the only consumer. Your backend should receive the store’s server-to-server notifications (App Store Server Notifications on iOS, Real-time developer notifications on Android) and reconcile them. Test that path on purpose. A subscription that renews in the store but never updates your database is one of the most common production incidents.
How do you simulate renewals, billing retry, expiry and refunds?
Map each state to the tool that reaches it most cheaply:
| State | Cheapest way to reach it |
|---|---|
| Trial or intro offer | StoreKit config file with an offer defined |
| Renewal | Accelerated local time rate, SKTestSession in tests, or sandbox timing |
| Billing retry or grace period | Local billing-failure toggle, or a declined test payment method on Play |
| Expiry | Let a sandbox subscription run out, or expire it in Transaction Manager |
| Refund or revocation | Refund in Transaction Manager, or a refund notification replayed to your backend |
| Plan change | Purchase a second product in the same group |
Billing retry deserves extra attention. When a renewal charge fails, the store keeps trying for a period, and during that time the user may or may not keep access depending on your configuration and the platform. Your app and server need a clear policy: do you keep access during grace, show a banner asking the user to fix payment, and cut access only on true expiry? Decide, write it down, and test each branch.
Refunds are the other state people skip. A refunded purchase should revoke the entitlement, and your server must handle the notification that says so. Test it by replaying a refund event against your webhook handler and checking that access is removed and the change is recorded.
How do you seed backend state?
You do not have to start each test by purchasing something. If your backend stores subscription state, create it directly:
INSERT INTO subscriptions (user_id, product_id, status, current_period_end)
VALUES
('user-trial', 'pro_monthly', 'trialing', now() + interval '3 days'),
('user-active', 'pro_monthly', 'active', now() + interval '20 days'),
('user-grace', 'pro_monthly', 'past_due', now() - interval '1 day'),
('user-expired', 'pro_monthly', 'expired', now() - interval '30 days'),
('user-refund', 'pro_monthly', 'revoked', now() - interval '5 days');
One user per state gives you stable fixtures. Log in as each and check the app’s behavior. This is faster than waiting for a sandbox renewal and is repeatable, which makes it suitable for automated tests.
Pair seeded state with replayed events. Keep a folder of real notification payloads (with identifiers removed) and post them to your webhook handler in tests. Two properties matter:
- Idempotency. The store may send the same notification more than once. Processing it twice must leave the same result as once.
- Ordering. Notifications can arrive out of order. A “renewed” event processed after a later “expired” event must not resurrect the subscription. Compare timestamps or transaction identifiers, not arrival order.
What can only be tested in a production-like environment?
Local and sandbox tests cannot fully prove several things. Plan for them explicitly:
- Real payment outcomes. Issuer declines, regional payment methods, tax handling and currency conversion behave differently with real cards.
- Real renewal timing. Accelerated clocks hide bugs that depend on real durations, such as time zones around a month boundary.
- Production signing and certificates. The keys and certificates used in production differ from test ones. Misconfigured verification is only visible there.
- Store review and product state. Products must be approved and available. A product that works in sandbox can still be missing or in the wrong state in production.
- Notification delivery at volume. Retries, delays and duplicate delivery show up with real traffic.
Reduce the risk with a staged rollout: ship to a small group using the production build and real products, such as internal testers on a beta channel, and watch the first real renewals closely. Add monitoring on the gap between store state and your database, and alert when they drift.
Key takeaways
- List your subscription states first, then test the transitions between them, on both the app and the server.
- Use StoreKit configuration files and the Transaction Manager for fast, offline iteration on iOS.
- Use the App Store sandbox and Google Play license testers to prove the real purchase and notification path.
- Seed one user per state in your database, and replay stored webhook payloads to test idempotency and ordering.
- Decide your billing-retry and refund policies explicitly, and test each branch.
- Accept that real payments, production keys and real renewal timing need a production-like check.
How does this relate to Edict?
Edict is a mobile backend in development. Its planned subscriptions module is designed to include a local development environment with mock subscriptions and simulated payment events, so entitlement logic can be exercised before any provider is connected. Simulated events do not prove that a real provider integration works, and validation against each provider’s own test environment will still be needed.
The fitness and habit tracking example, which gates a premium AI coach behind a subscription, is a good scenario to test this way. For the design behind the states you are testing, read why subscription entitlements belong on the server.