Testing the States That Break Mobile Apps
A production testing matrix for React Native apps: installation, authentication, offline behavior, purchases, lifecycle changes, and data migrations.
Most mobile bugs do not appear in a clean simulator with a newly created account and reliable Wi-Fi. They appear after an update, on a stale session, while a purchase is pending, after a request loses its response, or when the operating system resumes an app hours later. Testing those states is product engineering, not a final checklist.
Build a state matrix before writing more tests
List the states a person can actually enter, the transition that creates each state, and the expected user-visible result. This compact matrix is a useful starting point:
| Area | States worth exercising | What to verify |
|---|---|---|
| Installation | fresh install, return, update, reinstall, stale/corrupt local state | bootstrap, defaults, migration, safe recovery |
| Authentication | signed out, signed in, expired/revoked session, deleted account, provider cancellation/failure | route guards, refresh, error copy, local cleanup |
| Network | offline launch, disconnect mid-request, reconnect, slow response, timeout, partial backend failure | usable cached state, retry and honest status |
| Purchases | new purchase, cancellation, restore, existing/expired entitlement, store unavailable | no false downgrade; recoverable UI |
| Lifecycle | foreground/background, killed during work, resume after days, memory pressure | persisted state and resumed work |
| Versions | old schema, new build, partial migration | ordered migration and no silent data loss |
The matrix changes with the product. A fully local app has no cloud-sync conflict state; an account-backed app does. The goal is not exhaustive combinations. It is coverage of every transition that could lose work, lock someone out, charge them incorrectly, or make the app misleading.
Test at the right layer
Unit tests are fast feedback for pure parsing, reducers, selectors, migration transforms, and state transitions. They should not claim to prove a native authentication redirect or a store purchase.
Integration tests connect the pieces: a session listener changes navigation, a failed request creates a retryable pending operation, a migration transaction updates a schema version, or an entitlement update changes a feature gate. Use deterministic fixtures and controllable clocks/network responses. A test account that can be destroyed or reset is far safer than using a personal account.
Manual device tests catch the integration boundaries the test runtime cannot reproduce. Use development builds while integrating native modules, then preview/release-like builds for behavior that depends on signing, bundle configuration, redirects, notifications, or store services. TestFlight/internal testing is a separate release stage, not a synonym for a simulator run.
Installation and persisted state
Fresh install tests the startup path. Returning install tests restoration. Update tests an old persisted schema against the new binary. Reinstall tests the product's real recovery promise: local data may be gone, restored through OS backup, or recovered from an account, depending on the architecture. Never imply those are equivalent.
Create fixture databases for prior versions and run every migration from each supported version. Simulate an interruption after one migration step. For derived cache data, deleting and rebuilding can be safer than repairing; for user-created data, an explicit backup or transactional migration is usually needed. The architecture and migration rationale are expanded in Building an Offline-First React Native App.
Authentication is a lifecycle, not a boolean
Exercise signed-out startup, restored sign-in, expired token refresh, a revoked session, provider cancellation, and a deleted account. UI should wait for auth initialization rather than briefly rendering the wrong route. On logout or deletion, verify user-scoped local data is cleared or partitioned so the next user of the device cannot see it.
Run provider flows on actual devices and real release configuration. Redirect schemes, Apple credentials, and browser handoff behave differently from a mocked callback. Apple and Google Sign-In with Expo and Supabase explains why each build profile needs its own redirect verification; the account-deletion workflow adds a destructive-operation test case.
Network failure has several shapes
Airplane mode at launch checks local availability. Disconnecting during a request checks cancellation and durable pending work. A timeout checks the ambiguous case where the server may have completed the work. Reconnection checks retry order and conflict handling. A 500 response, DNS failure, authorization error, and validation error are not interchangeable—only transient cases should be automatically retried.
For an offline-capable screen, assert what it can still show and what status it communicates. Avoid treating reachability as proof that the backend works. Test slow responses too: a spinner must not block a previously useful screen, and repeat taps must not duplicate a command.
Purchases and lifecycle work outside your screen
Test a new purchase, a cancellation, restore on a new/reinstalled device, an entitlement that expires, and temporary store failure. A cancelled purchase is an expected outcome, not a crash dialog. Do not downgrade a paying user merely because the latest entitlement fetch failed while offline. The RevenueCat + Expo tutorial covers entitlement state, restore, sandbox behavior, and server authority.
During lifecycle testing, background the app mid-operation, force-close it, then relaunch. Resume after hours or days with an expired session and changed network. Treat memory pressure as a reason that any in-memory promise or navigation state may disappear; durable state should be recoverable from disk or the server.
Make diagnosis safe
Log state transitions, request IDs, build version, migration version, and coarse error categories that help diagnose a problem. Do not log access tokens, passwords, purchase receipts, full request bodies, or unnecessary personal data. A useful log makes a failure reproducible without creating a second privacy incident.
A release-focused loop
Turn incidents into fixtures
When a defect comes from an unusual state, capture the smallest reproducible fixture: app version, persisted-data version, account state, network condition, action sequence, and expected recovery. A vague test called “handles offline” tends to regress; a fixture named “migration from v3 with pending write and expired session” tells future maintainers what must remain true.
Release testing should also distinguish test environments from production behavior. Sandbox purchases, provider test credentials, preview APIs, and development redirect schemes are valuable, but each can conceal a configuration difference. Keep a concise release checklist that identifies which rows must be run against a signed internal build and which must be repeated in TestFlight or the equivalent store channel. If an operation changes server data, verify the server-side result too; a green client toast is not evidence that the intended state was persisted.
The matrix should influence design early. If an envisioned feature has no credible answer for “what if the app dies here?” or “what does this look like offline?”, either simplify it or create the missing durable state before polishing its happy path.
Before release, run automated checks, execute the highest-risk matrix rows on real devices, and record the exact build plus test-account state. Add a regression test whenever a production issue exposes a state you did not model. That is how testing becomes cumulative engineering knowledge rather than a ritual before the store upload. For distribution steps, see Shipping an Expo App to the App Store with EAS.
Related Tutorials
The full path from a working Expo project to an approved App Store release: build profiles, credentials, versioning, TestFlight, and the review rejections that are easiest to avoid.
How to add in-app purchases to an Expo app with RevenueCat: entitlements over product IDs, why restore is mandatory, and why the client is the wrong place to trust a purchase.
A production-minded approach to social sign-in in Expo: native Apple credentials, OAuth redirects, Supabase sessions, secure storage, and the identity edge cases that affect real users.
A practical architecture for React Native apps that remain useful without a connection: local source of truth, migrations, sync queues, conflicts, and honest failure handling.