State Transition Testing
Learn to test features whose behavior depends on "where they are right now", like orders, logins with lockout and music players. Draw state diagrams and state tables, test valid and invalid transitions, and hunt bugs in a broken order system.
Maya orders a pair of running shoes. They arrive on Tuesday, she wears them on Wednesday, and on Thursday she opens the shop app out of curiosity. The Cancel order button is still there. She taps it. The app says “Order canceled”, and three days later $84 lands back on her card. She keeps the shoes.
Nobody wrote code that said “give away free shoes”. The developer simply forgot to ask one question: should Cancel still work when the order is already delivered? The button did the same thing in every situation, when it should have depended on where the order was in its journey.
That question is the whole idea behind state transition testingState transition testingTesting how the system moves between states, like Locked, Active and Suspended.Open in glossary →.
What is state transition testing?
Some features always react the same way: a calculator adds 2 + 2 the same way every time. Many others remember where they are, and the same action gives a different result depending on that. Tapping Cancel on a fresh order should cancel it. Tapping it on a delivered order should not.
State transition testing is a black-boxBlack-box testingTesting based only on inputs and outputs, without looking at the code.Open in glossary → test design technique for those features. You draw the places the system can be in (its states), the things that move it between them (events), and then you test both the moves that should happen and the ones that should not.
The building blocks
| Word | Meaning | Music player example |
|---|---|---|
| State | A situation the system is in, which changes how it reacts | Stopped, Playing, Paused |
| Event | Something that happens: a tap, a timer, a server message | The user taps Pause |
| Transition | A move from one state to another, caused by an event | Playing → Paused |
| Guard | A condition that must be true for the transition to happen | Refund only if delivered ≤ 30 days ago |
| Action | What the system does while it moves | Remember the playback position |
Here is the music player drawn as a state diagram. Boxes are states, arrows are transitions, and the label on each arrow is the event. The small dot with an arrow marks the initial state (where everything starts).
Notice what is not on the diagram: there’s no arrow for “Pause while Stopped” or “Play while Playing”. Those are invalid transitions, and they are where a lot of bugs hide. Pressing Play twice in a row in a buggy player can start the same song twice, on top of itself.
The quick version
Two ways to write it down
A state diagram (like the one above) is great for seeing the flow. A state table is great for finding gaps. It lists every state as a row and every event as a column, so you’re forced to answer “what if this event happens in this state?” for every single combination:
| State ↓ / Event → | Play | Pause | Stop |
|---|---|---|---|
| Stopped | Playing | ✕ ignore | ✕ ignore |
| Playing | ✕ ignore | Paused | Stopped |
| Paused | Playing | ✕ ignore | Stopped |
Every ✕ cell is an invalid transition: the event must be rejected or ignored, and the state must not change. Diagrams quietly hide those cells. Tables put them in front of you.
How much to test: three levels of coverage
- Every state. Make sure you visit each state at least once. The weakest level.
- Every valid transition. Fire each arrow on the diagram at least once. This is the usual minimum for a feature.
- Every invalid transition. Try each ✕ cell and check the system says no. This is where the expensive bugs are.
Where it shines
Anything with a status or a mode in a mobile or web app:
- Orders: Placed → Paid → Shipped → Delivered, plus Canceled and Refunded. See shopping cart and checkout.
- Login lockout: 0, 1, 2 wrong passwords, then Locked for 15 minutes. See login and sign-up.
- Players and trackers: Stopped / Playing / Paused, or a workout that is Running / Paused / Finished.
- Trips and deliveries: a ride that is Requested → Driver assigned → On trip → Completed.
- Cards and accounts: Active, Blocked, Expired.
Typical bugs it finds
- An invalid transition is allowed: canceling a delivered order, paying twice, refunding an unpaid order.
- A stuck state: the order stays “Processing” forever after a network error, and no button gets you out.
- The wrong state after restart: you pause a workout, the app is killed in the background, and it comes back “Running”. See background and foreground.
The building blocks in depth
States
A state is a situation that changes how the system reacts to the same input. That definition is the test for whether something deserves to be a state.
- “The order is paid” is a state, because Pay, Cancel and Refund all behave differently once it’s true.
- “The order has a blue shipping label” is not a state if nothing behaves differently because of it.
Two special kinds of state appear in almost every model:
- The initial state is where the system starts: a new order is Placed, a fresh login screen has 0 wrong attempts. On a diagram it’s marked with a small filled dot and an arrow.
- A final state is one the system never leaves: a Canceled or Refunded order. It’s drawn with a double border. Final states are great places to test, because every event there should be rejected.
Events and transitions
An event is anything that can happen to the system. Some come from the user (tap Cancel), some from time (15 minutes pass, the paid parking time runs out), and some from other systems (the courier’s scanner says “delivered”, the bank confirms a payment, a push notificationPush notificationA message an app sends to the device that appears even when the app is closed.Open in glossary → arrives).
A transition is the arrow: in state A, when event E happens, go to state B. A transition can also go back into the same state. A bank card that is Active stays Active after a successful payment, but something still happens: money moves. That’s called a self-transition.
Guards
A guard is a yes/no condition written in square brackets on an arrow, like refund [≤ 30 days since delivery]. The same event in the same state can go different ways depending on the guard:
- Delivered + Refund [within 30 days] → Refunded
- Delivered + Refund [after 30 days] → stays Delivered, with the message “The return window has closed”
Guards are where state transition testing meets boundary value analysisBoundary value analysisTesting values at and just around the edges of valid ranges, where bugs love to hide.Open in glossary →: test day 30 and day 31, not just day 5. And where several conditions combine (amount, country, membership level), a decision tableDecision tableA table listing combinations of conditions and the action the software should take for each.Open in glossary → is the better tool for the guard itself. See boundary value analysis and decision tables.
Actions
An action is what the system does during a transition: charge the card, send the tracking email, reset the counter, release the reserved stock. Written after a slash: cancel / refund payment.
Actions matter because a transition can land in the right state and still do the wrong thing. If canceling a paid order shows “Canceled” but never refunds the money, the state is correct and the customer is still out $84. Always check the state and the action in your expected resultExpected resultWhat should happen when you perform a test step, according to the requirements.Open in glossary →.
Drawing a state diagram from a spec
You don’t need special software. Paper, a whiteboard or any diagram tool works. Here’s the method, using an online shop’s order rules.
- Find the nouns that describe a status. Read the requirementsRequirementA statement of what the software must do or how well it must do it.Open in glossary → and highlight words like placed, paid, shipped, delivered, canceled, refunded. Those are your candidate states.
- Find the verbs and the timers. Pay, ship, deliver, cancel, refund, 30 days pass. Those are your events.
- Draw the happy path first. Placed → Paid → Shipped → Delivered.
- Add the side exits. Where can the order be canceled? Where can it be refunded? Add guards and actions as you go.
- Mark the initial and final states. Where does it start? Which states are dead ends?
- Turn it into a table and look at the empty cells. Every empty cell is a question for the product owner: “What should happen if a customer taps Cancel while the order is Shipped?” If nobody knows, you’ve found a gap in the spec before writing a single test.
State tables: where the bugs hide
The diagram above has 6 states and 5 events. That’s 6 × 5 = 30 combinations, but only 6 arrows. The other 24 combinations are invalid transitions. A diagram doesn’t show them, so nobody thinks about them. A state table does.
You’ll build the full order table in the explorer below (switch it to State table). Here’s a compact version of the most interesting rows:
| State | Cancel | Refund | Pay |
|---|---|---|---|
| Placed | Canceled | ✕ nothing was paid | Paid |
| Paid | Canceled / refund | ✕ use Cancel | ✕ already paid |
| Shipped | ✕ offer a return | ✕ not delivered | ✕ already paid |
| Delivered | ✕ too late | Refunded [≤ 30 days] | ✕ already paid |
For every ✕ cell, a correct system does three things:
- Stays in the same state. Nothing moves.
- Does no action. No money moves, no email is sent, no stock is released.
- Tells the user why (if the user caused it), or hides/disables the control in the first place. See buttons and their disabled stateDisabled stateA component that is visible but can't be used right now, usually shown greyed out.Open in glossary →.
How much to test: coverage levels
CoverageTest coverageHow much of the product, requirements or risks your tests actually check.Open in glossary → tells you how much of the model your tests have touched. From weakest to strongest:
| Level | What you test | Order example |
|---|---|---|
| All states | Each state at least once | 6 states, reachable in 2 tests (one ending in Canceled, one in Refunded) |
| All valid transitions | Each arrow at least once | 6 arrows, coverable in 3 tests; the usual minimum |
| All invalid transitions | Each ✕ cell at least once | 24 cells, the high-value negative tests |
Visiting every state is not enough. You can visit all six order states without ever trying “cancel a paid order”, and miss that it forgets to refund.
Where it shines in mobile and web apps
Order status is the classic. The same rules usually show up in three places: the customer app, the staff back office, and emails or push notifications. Each one is a door into the same state machine. Things to try:
- Cancel at every state, not just Placed.
- Pay twice: double-tap Pay, or pay from two tabs. This is a double submitDouble submitA bug where an action runs twice because the user tapped or clicked twice quickly.Open in glossary → and the server needs idempotencyIdempotencyAn action that gives the same result no matter how many times it is repeated.Open in glossary → to stop it.
- Refund at every state, especially before payment and after the 30-day guard.
More in shopping cart and checkout.
A login with lockout turns a counter into states: 0 wrong, 1 wrong, 2 wrong, Locked. It protects against a brute-force attackBrute-force attackGuessing passwords or codes over and over until one works.Open in glossary →.
- The third wrong password locks the account (not the second, not the fourth: a boundary!).
- The right password while Locked must still be rejected.
- A successful login resets the counter. Does it? Test 2 wrong → right → log out → 1 wrong: the account must not lock.
- After 15 minutes, can you log in again? What if you kill the app during the lockout?
More in login and sign-up.
A ride-hailing trip has states driven by three different people: the rider, the driver and the system.
- Can the rider cancel On trip? (No: the trip must be ended by drop-off.)
- What happens if the driver accepts at the exact moment the rider cancels? This is a race conditionRace conditionA bug that depends on the timing or order of events, so it only happens sometimes.Open in glossary →, and only one of them can win.
- Is the late-cancel fee charged at 1:59 and at 2:01 correctly?
A bank card in a banking app: Active, Blocked (frozen by the user) and Expired.
- Paying while Blocked must be declined. This is the whole point of the freeze button.
- Unblock needs Face ID or the PIN (the guard). Test it with the check failing.
- An Expired card must not come back to life when someone taps Unblock.
Turning transitions into test cases
Each transition (valid or invalid) becomes one test caseTest caseA written set of steps, data and an expected result used to check one specific behavior.Open in glossary →. The structure maps neatly:
| Test case part | Comes from |
|---|---|
| PreconditionPreconditionSomething that must already be true before you can start a test.Open in glossary → | The start state (and how to get there) |
| Step | The event (and the guard value) |
| Expected result | The end state + the action (or “stays, nothing happens”) |
- Pick a transition, for example Paid + Cancel.
- Write the precondition as a path from the initial state: “Order placed and paid with a test card.”
- Write the step: “Tap Cancel order and confirm.”
- Write both halves of the expected result: “Status is Canceled and a refund of $84.00 appears in Payments.”
- Chain transitions to save time. One test can walk Placed → Paid → Shipped → Delivered → Refunded and check each step. Just make sure a failure in step 2 doesn’t hide steps 3 to 5.
Typical bugs
| # | Bug | What you see | Typical severity |
|---|---|---|---|
| 1 | Invalid transition allowed | Delivered order can be canceled and refunded | Critical |
| 2 | Paying twice | Double-tap Pay, card charged twice | Critical |
| 3 | Refund on an unpaid order | Refund issued for money never received | Critical |
| 4 | Lockout ignores the right password | Locked account still logs in with the right password | Critical |
| 5 | Stuck state | Order stays “Processing” forever after a network error | Major |
| 6 | Wrong state after restart | Paused workout comes back as Running after the app is killed | Major |
| 7 | Action missing | Order shows Canceled but no refund is made | Major |
| 8 | Final state revived | Old payment link re-opens a canceled order | Major |
Bugs 5 and 6 deserve a special mention for mobile. Apps are paused, killed and restored all the time, and the state must come back correctly from the server or from saved data. That’s state persistenceState persistenceWhether the app remembers what the user did after navigating away, rotating, or restarting.Open in glossary → and state restorationState restorationBringing the user back to the same screen and data after the OS closed the app in the background.Open in glossary →. Test transitions across a restart: start in Paused, kill the app, reopen, and check you’re still in Paused. See background and foreground and interruptions.
When you find one of these, the state table makes your bug report easy to write: start state, event, expected state, actual state.
When to use it (and when not to)
Use it when the same input gives different results depending on history: statuses, modes, counters, wizards, sessions, players, anything with “if it’s already X, then…”.
Skip it when the feature has no memory: a unit converter, a tip calculator, a search box. There, equivalence partitioningEquivalence partitioningSplitting inputs into groups that should behave the same, then testing one value from each group.Open in glossary → and boundary value analysisBoundary value analysisTesting values at and just around the edges of valid ranges, where bugs love to hide.Open in glossary → do the work.
Try it: the state machine explorer
This is the heart of the lesson. Pick a model, fire events, and watch the current state move. Arrows turn green once you’ve covered them. Then do what a good tester does: fire events that shouldn’t work and read what a correct system must do.
🧭 State machine explorer
Pick a model, fire events and watch where the system goes. Try to fill all three coverage meters.
A shop order from checkout to doorstep. Two final states: Canceled and Refunded.
Current state
Placed
Fire an event
Tip: don't only follow the happy path. Fire events that "shouldn't" work, like Refund.
Coverage
Guards tested as false: 0/1
Every ✕ cell in the state table.
Event log
No events yet. Each line here is one test step.
Find the bug: a broken order system
Below is a shop’s back-office order page. The staff tool shows every action button all the time, so the server is responsible for rejecting the ones that aren’t allowed. Compare what it does with the order diagram and state table (the explorer above shows the spec). Use New order to start again. The notebook keeps your coverage across orders, so you can be systematic: try every action in every state.
🐞 Bug hunt: Order back office
This demo has 5 hidden bugs. Explore it like a tester, then check your findings.
Order
#A-1042
Actions
Staff tool: every action is always shown. The server must reject the ones that aren't allowed.
Payments
No payments yet.
Net: $0.00
Status history
- Placed09:12
Tester's notebook
Coverage is measured against the spec diagram above, across all orders you create.
- States
- 1/6
- Valid
- 0/6
- Invalid tried
- 0/24
- 1
Hint: Get an order all the way to Delivered, then try the red buttons.
- 2
Hint: On a brand-new order, look at what Refund does to Payments.
- 3
Hint: What happens if you skip Record payment?
- 4
Hint: Canceled is a final state. Is it really final?
- 5
Hint: Try the same action twice in a row.
Practice: build a state table
Time to do it yourself. A parking app called ParkEasy lets drivers pay for street parking by the hour. Read its 7 rules and fill in the state table. Every cell is either the next state or ✕ (the event must be rejected).
🅿️ Exercise: the parking session state table
Read the spec, then fill every cell: which state does the session move to? Use ✕ when the event must be rejected.
Spec: ParkEasy app
- Tapping Start creates a session in Active.
- An Active session can be extended once → Extended.
- Only one extension per session is allowed.
- The driver can Stop an Active or Extended session → Ended.
- When the paid time runs out in Active or Extended → Expired.
- An Expired session can't be extended. Stop closes it → Ended (receipt is created).
- Ended is final: nothing changes it.
1. Pick a value
2. Tap cells to fill them
| State | Extend | Stop | Time runs out |
|---|---|---|---|
| Active | |||
| Extended | |||
| Expired | |||
| Ended |
Once your table is right, you’ve also written your test plan: 12 cells = 12 test cases, 6 positive and 6 negative. Some extra ideas a curious tester would add:
- What if “Time runs out” and “Stop” happen in the same second? Which one wins, and is the driver charged correctly?
- Does Extended survive killing the app and reopening it the next morning?
- Is the receipt created exactly once when an Expired session is stopped?
Example test cases
TC-ST-01Order happy path: Placed to Deliveredpositivehigh
Preconditions
- Logged in as a customer
- Cart with 1 item ($84.00 incl. shipping)
Steps
- Place the order
- Pay with a test card
- Staff marks it Shipped
- Courier marks it Delivered
Expected result
Status goes Placed → Paid → Shipped → Delivered. One charge of $84.00, a tracking email after Shipped, a push notification after Delivered.
TC-ST-02Cancel a paid order refunds the paymentpositivehigh
Preconditions
- Order is Paid ($84.00)
Steps
- Open the order
- Tap Cancel order and confirm
Expected result
Status is Canceled and a refund of $84.00 is created. Reserved stock is released.
TC-ST-03Cancel is rejected for a delivered ordernegativehigh
Preconditions
- Order is Delivered
Steps
- Open the order in the app
- Check whether Cancel is offered
- Send a cancel request another way (an older tab or the API)
Expected result
The app offers Return & refund instead of Cancel. The cancel request is rejected; status stays Delivered and no refund is created.
TC-ST-04Paying twice charges only oncenegativehigh
Preconditions
- Order is Placed
- Network throttled to a slow 3G profile
Steps
- Double-tap Pay
- Open the order in a second tab and pay again
Expected result
Exactly one charge of $84.00. The second attempt shows "This order is already paid" and the status stays Paid.
TC-ST-05Right password is rejected while the account is lockednegativehigh
Preconditions
- Test account exists
- 3 wrong passwords entered, account is Locked
Steps
- Enter the correct password
- Tap Log in
Expected result
Login is refused with "Too many attempts. Try again in 15 minutes." The account stays Locked.
TC-ST-06Successful login resets the wrong-password counteredgemedium
Preconditions
- Test account exists
- 0 wrong attempts
Steps
- Enter a wrong password twice
- Enter the right password
- Log out
- Enter a wrong password once
Expected result
After the last step the account is in "1 wrong" and is NOT locked.
TC-ST-07Refund is blocked after the return window (guard false)edgemedium
Preconditions
- Order Delivered 31 days ago
Steps
- Open the order
- Request a refund
Expected result
Refund is refused with "The return window has closed". Status stays Delivered; no refund is created. (Repeat with an order delivered exactly 30 days ago: refund is allowed.)
TC-ST-08Paused state survives the app being killededgemedium
Preconditions
- Fitness app, a workout is Paused at 12:30
Steps
- Send the app to the background
- Kill it from the app switcher
- Reopen the app
Expected result
The workout is still Paused at 12:30, not Running and not lost.
Tip: click a test case to expand it.
Checklist
State transition testing checklist
0 of 12 checked
Check your understanding
Quick quiz
0/6 answered · 0 correct
1.In a music player, the user taps Pause while it's Playing and it moves to Paused. What is 'tap Pause' in state transition terms?
2.Which notation describes a guard?
3.Your tests visit all six order states. What can you honestly claim?
4.Why is a state table better than a diagram for finding invalid transitions?
5.A login locks after 3 wrong passwords. Which test is an invalid transition test?
6.Paid + Pay is fired. The status pill still says Paid. Is that enough to pass the test?
Key takeaways
- Use state transition testing when the same action gives different results depending on where the system is: orders, logins, players, trips, cards.
- The building blocks are states, events, transitions, guards (conditions in brackets) and actions (what happens during the move).
- Diagrams show the flow; state tables expose every “what if this event happens in this state?” combination.
- Coverage grows from all states to all valid transitions to invalid transitions. The invalid ones find the costly bugs.
- Each transition becomes a test case: start state as precondition, event as step, end state plus action as expected result.
- Watch for allowed invalid transitions, stuck states and the wrong state after an app restart.
Finished this page?Nice work, this page is complete!
Mark it as done to track your progress. It's saved in this browser, no account needed.
Its review cards are now in Bites ⚡