State Transition Testing — Verifying System Behaviour Across States

Many systems behave differently depending on their current state. An online order can be Draft, Submitted, Approved, Shipped, Delivered, or Cancelled — and the actions available to the user change depending on which state the order is in. You can cancel a Draft order but not a Delivered one. You can approve a Submitted order but not a Shipped one. State transition testing models these states and the transitions between them to verify that the system allows valid transitions, blocks invalid ones, and updates its state correctly after each action.

State Transition Testing — Modelling Dynamic Behaviour

A state transition model has four components: states (the conditions the system can be in), events (triggers that cause transitions), transitions (the valid paths between states), and actions (what the system does during a transition). Test cases are designed to exercise every valid transition and attempt every invalid one.

# State Transition Model: Online Order Management

STATES = ["Draft", "Submitted", "Approved", "Shipped", "Delivered", "Cancelled"]

# Valid transitions: (from_state, event) → (to_state, action)
VALID_TRANSITIONS = [
    ("Draft",     "Submit",    "Submitted",  "Send confirmation email"),
    ("Draft",     "Cancel",    "Cancelled",  "No charge; remove from queue"),
    ("Submitted", "Approve",   "Approved",   "Charge payment; notify warehouse"),
    ("Submitted", "Reject",    "Draft",      "Send rejection reason to customer"),
    ("Submitted", "Cancel",    "Cancelled",  "Refund if charged; notify customer"),
    ("Approved",  "Ship",      "Shipped",    "Update tracking; notify customer"),
    ("Approved",  "Cancel",    "Cancelled",  "Full refund; notify warehouse"),
    ("Shipped",   "Deliver",   "Delivered",  "Confirm delivery; close order"),
    ("Shipped",   "Return",    "Approved",   "Process return; schedule pickup"),
]

# Invalid transitions — these should be BLOCKED by the system
INVALID_TRANSITIONS = [
    ("Delivered",  "Cancel",  "Cannot cancel a delivered order"),
    ("Delivered",  "Ship",    "Cannot re-ship a delivered order"),
    ("Cancelled",  "Approve", "Cannot approve a cancelled order"),
    ("Cancelled",  "Ship",    "Cannot ship a cancelled order"),
    ("Draft",      "Ship",    "Cannot ship without approval"),
    ("Draft",      "Deliver", "Cannot deliver a draft order"),
    ("Shipped",    "Approve", "Already approved — cannot re-approve"),
]

print("State Transition Model — Order Management")
print("=" * 70)
print(f"\nStates: {', '.join(STATES)}")

print(f"\nValid Transitions ({len(VALID_TRANSITIONS)}):")
print(f"{'From':<12} {'Event':<10} {'To':<12} {'Action'}")
print("-" * 70)
for from_s, event, to_s, action in VALID_TRANSITIONS:
    print(f"{from_s:<12} {event:<10} {to_s:<12} {action}")

print(f"\nInvalid Transitions to TEST ({len(INVALID_TRANSITIONS)}):")
print(f"{'From':<12} {'Event':<10} {'Expected Behaviour'}")
print("-" * 70)
for from_s, event, reason in INVALID_TRANSITIONS:
    print(f"{from_s:<12} {event:<10} BLOCKED — {reason}")

# Derive test cases
print(f"\n\nTest Case Summary:")
print(f"  Valid transition tests:   {len(VALID_TRANSITIONS)} (verify state changes correctly)")
print(f"  Invalid transition tests: {len(INVALID_TRANSITIONS)} (verify system blocks them)")
print(f"  Total:                    {len(VALID_TRANSITIONS) + len(INVALID_TRANSITIONS)} test cases")

# Sequence testing — multi-step paths through states
print(f"\n  Example valid path: Draft → Submit → Approve → Ship → Deliver")
print(f"  Example cancel path: Submitted → Cancel (refund issued)")
print(f"  Example reject path: Submitted → Reject → Draft (re-edit)")
Note: Testing invalid transitions is just as important as testing valid ones. If the system allows cancelling a delivered order, the business might issue refunds for items the customer already has. If it allows shipping a draft order, products ship without payment. Invalid transition tests verify that the system enforces its state machine rules — they are essentially security and integrity tests for the workflow logic.
Tip: Draw the state transition diagram before writing test cases. A visual diagram with circles (states) and arrows (transitions) makes it easy to spot missing transitions, dead-end states (states with no outgoing transitions that should have them), and unreachable states (states with no incoming transitions). Share the diagram with the product owner to validate the model — they can spot business logic errors in a diagram far more easily than in a requirements document.
Warning: Do not assume that only the "happy path" sequence needs testing. Real users do not follow linear paths — they cancel mid-flow, go back, retry after errors, and take unexpected detours. Test multi-step sequences that include cancellations, rejections, retries, and loops (e.g., Submit → Reject → Edit → Re-submit → Approve). These non-linear paths are where state management bugs hide because developers often optimise for the expected flow and handle edge paths as afterthoughts.

Common Mistakes

Mistake 1 — Only testing the happy path through the state machine

❌ Wrong: Testing only Draft → Submit → Approve → Ship → Deliver and declaring state transition testing complete.

✅ Correct: Testing every valid transition individually, every invalid transition (attempting to perform actions that should be blocked), and key multi-step sequences including cancellations, rejections, and retries.

Mistake 2 — Not verifying that the system's state actually changes

❌ Wrong: Clicking "Submit" and seeing a success message, then assuming the order is in the "Submitted" state without verifying.

✅ Correct: After each transition, verifying the new state by checking the order status display, the available actions (Cancel should be available; Ship should not), and the database or API response that confirms the state change.

🧠 Test Yourself

An order management system has 6 states and 9 valid transitions. During testing, you attempt to cancel a Delivered order and the system allows it, issuing a full refund. What type of defect is this?