State Transition Testing
Concept
State transition testing verifies that a system moves correctly between states.
It tests both valid transitions (allowed paths) and invalid transitions (should be rejected).
State A --[event]--> State B
State A --[invalid event]--> stays in State A (rejected)
Key Elements
Element
Description
State
A condition the system can be in
Event / Trigger
An action or input that causes a transition
Transition
A move from one state to another
Guard
A condition that must be true for the transition
Action
What happens during a transition
State Transition Diagram: Order Lifecycle
pending → confirmed → shipped → delivered
↓ ↓
cancelled cancelled
Valid transitions:
- pending → confirmed, cancelled
- confirmed → shipped, cancelled
- shipped → delivered
- delivered → (none)
- cancelled → (none)
Implementation + Tests
class Order :
TRANSITIONS : dict [ str , list [ str ]] = {
"pending" : [ "confirmed" , "cancelled" ],
"confirmed" : [ "shipped" , "cancelled" ],
"shipped" : [ "delivered" ],
"delivered" : [],
"cancelled" : [],
}
def __init__ ( self ) -> None :
self . state = "pending"
def transition ( self , new_state : str ) -> bool :
if new_state in self . TRANSITIONS [ self . state ]:
self . state = new_state
return True
return False
class TestOrderStateTransition :
def test_valid_full_flow ( self ) -> None :
order = Order ()
assert order . transition ( "confirmed" )
assert order . transition ( "shipped" )
assert order . transition ( "delivered" )
assert order . state == "delivered"
def test_cannot_ship_pending_order ( self ) -> None :
order = Order ()
assert not order . transition ( "shipped" )
assert order . state == "pending"
def test_cannot_reverse_delivered ( self ) -> None :
order = Order ()
order . transition ( "confirmed" )
order . transition ( "shipped" )
order . transition ( "delivered" )
assert not order . transition ( "pending" )
def test_cancel_from_pending ( self ) -> None :
order = Order ()
assert order . transition ( "cancelled" )
assert order . state == "cancelled"
def test_cancel_from_confirmed ( self ) -> None :
order = Order ()
order . transition ( "confirmed" )
assert order . transition ( "cancelled" )
assert order . state == "cancelled"
def test_cannot_cancel_after_shipped ( self ) -> None :
order = Order ()
order . transition ( "confirmed" )
order . transition ( "shipped" )
assert not order . transition ( "cancelled" )
Coverage Levels
Level
What to Cover
Effort
0-switch
All states visited
Low
1-switch
All valid transitions
Medium
2-switch
All pairs of transitions
High
Invalid
All invalid transitions
Must have
For most production systems, 1-switch + all invalid transitions gives strong coverage.
State Transition Table
Current State
Target State
Result
Valid
pending
confirmed
confirmed
Yes
pending
shipped
pending
No
pending
cancelled
cancelled
Yes
confirmed
shipped
shipped
Yes
confirmed
cancelled
cancelled
Yes
confirmed
delivered
confirmed
No
shipped
delivered
delivered
Yes
shipped
cancelled
shipped
No
delivered
any
delivered
No
cancelled
any
cancelled
No
When to Use
Scenario
Use STT
Order / booking / workflow status
Yes
Authentication flow (login → verified → active)
Yes
Game states / UI wizard steps
Yes
Simple CRUD without status field
No
Stateless API endpoints
No
Risks
Risk
Description
Mitigation
Missing invalid transitions
Only testing happy paths
Build full state transition table
Shared order instance
Tests pollute each other
Fresh instance per test
Undocumented states
States exist in code but not in spec
Map state machine from code, not just docs
test-design
testing