Key takeaways
- State transition testing verifies that your app moves correctly between states as events happen. Login attempts, session expiry, backgrounding, network drops. Each is an event that should transition app somewhere sane.
- Mobile makes this harder than textbooks admit. You're not testing one state machine. You're testing five parallel ones (app lifecycle, session, network, permissions, screen state) that can transition at same time.
- The state explosion problem is real. You can't test every combination. You test ones tied to real failure modes: session expires while offline, permission revoked mid-flow, background-then-kill during checkout.
State transition testing is a black-box technique where you model your app as a set of states, define events that cause transitions between them, and write tests that walk through both valid and invalid transitions to see if app handles each one correctly.
On paper this is simple. Draw a diagram. Cover every arrow with a test. Done.
On mobile it gets messy fast. Your app isn't one state machine. It's several running in parallel, transitioning at different speeds, sometimes triggering each other. The interesting bugs live at intersections.
Here's how to actually do state transition testing for a mobile app without losing rest of your sprint to a state diagram nobody reads.
What it actually models
Every state transition test needs four things.
States. Discrete conditions your app can be in. "Logged out." "Logged in, cart empty." "Logged in, cart has 3 items." "Payment in progress." Each is a real snapshot app can hold.
Events. Things that trigger state changes. User taps a button. Session token expires. Network drops. Push notification arrives. App gets backgrounded.
Transitions. The rules for how events move app between states. "Logged out + valid credentials event = Logged in." "Logged in + logout event = Logged out."
Actions. What app does when a transition fires. Renders a new screen. Fires an analytics event. Writes to local storage. Shows a toast.
Those are pieces. The ISTQB glossary has strict definition if you want it. In practice, most teams don't need strict version. They need enough model to catch bugs.
State transition testing on mobile is harder
Every generic tutorial uses an ATM or a vending machine as example. Those are single-machine cases. Card in, PIN in, PIN out, cash out. One state at a time.
Mobile apps run several state machines simultaneously. At any given moment, your app has:
- App lifecycle state. Foreground, background, suspended, killed.
- Session state. Anonymous, authenticated, token-refreshing, expired.
- Network state. Online, offline, slow, transitioning.
- Permission state. For each permission, granted or denied. Each can flip via Settings.
- Screen state. Whatever route user is currently viewing.
Each of these is its own state machine. Each has its own events. They also interact. Backgrounding app can trigger a session timeout. A network drop while backgrounded triggers different behavior than a drop while foregrounded. Permission revocation via Settings triggers a different code path than declining a permission prompt in-app.
If you draw a full state diagram of all five machines and their interactions, you get 200+ states and 800 transitions. Nobody's writing tests for all that.
Apps with multi-step flows benefit from state transition testing written case by case rather than diagram-first. Model specific flow. Test transitions that matter for that flow. Skip rest.
Concurrent user actions also push apps through unexpected transitions, so state transition testing and concurrency testing tend to pair for anything with payment or auth.
The real transitions to test on mobile
Here are mobile-specific transitions where bugs actually live.
App lifecycle transitions
Foreground to background to foreground. User taps home, waits 30 seconds, taps back. Your app should resume where they left off. Did it? Or did it lose form they were filling out?
Foreground to killed to relaunched. OS kills your app in background for memory. User taps icon. State restoration should kick in. On Android this is Bundle. On iOS this is NSCoding or SwiftUI's SceneStorage. If restoration doesn't work, they land on home screen instead of checkout screen they were on.
Cold start vs warm start. Cold start goes through your entire initialization. Warm start skips it. Different code paths. Different bugs.
Session transitions
Token valid to token expired. What happens mid-flow when token dies. Silent refresh. Force logout. Retry failed call. Whatever your app does, test it explicitly.
Anonymous to authenticated mid-session. User was browsing without logging in, adds items to cart, then logs in. Does cart survive transition? Is local anonymous cart merged with any existing server cart? What if they had different items in both?
Authenticated to logged out via server. Admin revoked session server-side. Next API call returns 401. Does your app handle this or does it show a blank screen and confuse user.
Network transitions
Online to offline while writing. User taps "Save note" with a wobbly connection. Request hangs. Network drops. What state is note in?
Offline to online with pending writes. User made 3 edits while offline. Comes back online. Do edits sync in order? What if two of them conflict?
Slow network transitioning to normal. User's on 3G. App started slow. Wi-Fi kicks in mid-page-load. Do in-flight requests get abandoned or completed?
Permission transitions
Granted to denied via Settings. User granted location, then revoked it from iOS Settings. Comes back to app. Does app check current permission state or does it assume last state it saw?
Denied then requested again. iOS caps how many times you can request. Android varies by version. Test both platforms because behavior differs.
Compound transitions (this is where bugs are)
Session token expires while app is backgrounded. User foregrounds it. What happens first, token refresh or resume?
Payment in progress when phone rings. iOS interrupt handling. Does payment complete when call ends?
User revokes camera permission while camera preview is active. Different than revoking while camera is inactive. Both should not crash.
Crashes are just another state transition, which is why state transition testing surfaces recovery bugs that other techniques miss. The crashed β relaunched transition is a state boundary. Test it explicitly.
Building a state transition table for a real flow
Skip ATM example. Here's a real one for a food delivery app checkout.
States: Cart, Delivery address selection, Payment method selection, Payment processing, Order confirmed, Order failed.
Events: Tap next, tap back, payment approved, payment declined, network drops, session expires, app backgrounded.
Nine rows. Nine tests. Each test starts at Current state, fires Event, and verifies you land in Next state with Action having happened.
The interesting rows are last three. Network drops during payment. App backgrounded during payment. Session expires during payment. These are ones that cause "double charged" bugs and "money left account but no order" bugs. If you skip these you'll still find them, just in production.
Managing state explosion
Every generic post mentions state explosion. Very few give practical advice.
Rules that work:
Model one flow at a time. Not whole app. Checkout is one model. Onboarding is another. Login-and-session is a third. Keep them separate.
Cap states at 8 per flow. More than that and nobody will maintain it. Merge states that share same set of outgoing events.
Ignore transitions you can't afford to test. State transition testing gets you invalid transition coverage (things that should never happen). On mobile you have to be pragmatic. Test invalid transitions tied to real bugs, skip theoretical ones.
Diagram in code, not Miro. Put transition table in your test file as a data structure. It stays in sync with tests. A diagram in Miro goes stale in three weeks.
Use event-based test data. Instead of "test state A to state B", write "test that Event X from State A moves us to State B." Then you can add new states without rewriting every test.
Where automated state transition testing breaks
The tests themselves are conceptually simple. Get to a state. Trigger an event. Verify next state. On mobile middle step is where things break.
"Trigger an event" for a network drop means actually dropping network. For a session expiry means actually letting token expire or forcing it. For a background transition means actually backgrounding app. Emulators fake some of this. Real devices with a proxy in middle do it properly.
Selector-based frameworks (Appium, XCUITest, Espresso) make "verify next state" part fragile. Each state has its own screen. Each screen has its own locators. Screen changes and layouts drift, and your state transition tests break for cosmetic reasons unrelated to whether transition worked.
A state transition test that says "after Payment processing, we should reach Order confirmed" fails because accessibility ID for confirmation heading changed from order_conf_title to order_confirmation_title in a UI cleanup. The transition worked. The test broke. Nobody wrote a bug ticket. You spent 45 minutes debugging what turned out to be one line of manifest that got renamed.
What works for mobile state transition testing at scale
We built Drizz partly for this shape of problem. Tests are written as plain English commands. Tap "Pay now". Validate "Order confirmed" is visible. Vision AI reads screen way a person would. When you're verifying state B by looking for "Order confirmed" on screen, Vision AI sees text regardless of which accessibility ID it lives under. The state transition test verifies actual thing it's supposed to verify.
Concrete before-and-after on "Payment processing β Order confirmed" transition.
Before, on Appium: Test locates confirmation heading by ID. UI team renames ID during a design refactor. Test fails on state transition even though transition itself works. QA lead assumes it's a real bug. Files a ticket. Two hours later dev team explains it was a rename. 45 minutes to update test.
After, on Drizz: Same test, plain English. Validate "Order confirmed" is visible. UI team renames accessibility ID. Test still passes because visible text is still "Order confirmed." State transition is verified correctly.
That's difference between framework being obstacle to test and framework getting out of way.
For teams doing state transition testing on mobile flows, framework choice determines whether you can actually test transitions or whether you spend most of your time debugging your own selectors while transitions themselves rot.
FAQ
What is state transition testing?
A technique to model an app as states and events, then test each event moves to correct next state.
How do you use state transition testing on a mobile app?
Model one flow at a time. List its states, events, and transitions in a table. Write a test per row.
What's difference between state transition testing and flow testing?
Flow testing walks a happy path end to end. State transition testing covers both valid and invalid transitions between states.
Why is state transition testing harder on mobile?
Mobile apps run several state machines in parallel: lifecycle, session, network, permissions, screen. The interesting bugs live at their intersections.
How do you handle state explosion in mobile testing?
Model one flow at a time, cap states at 8, and skip invalid transitions not tied to real failure modes.
Can state transition tests be automated?
Yes, but selector-based frameworks make "verify next state" step fragile. Vision-based tests are more stable across UI changes.
β


