State Transition Testing
For systems that behave differently depending on what already happened, test the transitions between states — including the ones that should be rejected.
What you'll learn
- Model a feature's valid states and the transitions allowed between them
- Design test cases for both valid and invalid transitions
- Explain why testing only valid transitions leaves a system's guardrails unverified
Prerequisites
Explanation
An order can be pending, paid, shipped, or cancelled. The action "cancel" should succeed from pending or paid, but must be rejected from shipped — you cannot cancel an order that's already on a truck. The same action is correct in some states and a bug (or a real-world impossibility) in others. This is exactly the shape of problem the earlier techniques don't cover: equivalence partitioning and boundary-value analysis reason about a single input in isolation; they say nothing about what already happened before this input arrived.
State transition testing starts by explicitly modeling the system as a set of states and the transitions allowed between them — often drawn as a diagram, but just as usefully written as a simple table: current state, action taken, resulting state (or "rejected"). For the order example:
| Current state | Action | Result |
|---|---|---|
| pending | pay | paid |
| pending | cancel | cancelled |
| paid | ship | shipped |
| paid | cancel | cancelled |
| shipped | cancel | rejected — cannot cancel a shipped order |
| cancelled | pay | rejected — cannot pay a cancelled order |
Two categories of test case come out of this table, and both matter: valid transitions (does "pay" correctly move a pending order to paid?) and, just as important, invalid transitions (does the system correctly reject cancelling a shipped order, rather than silently allowing it or crashing?). A system that only ever gets tested on the "happy path" sequence of valid transitions has never had its guardrails — the rules about what must not be allowed to happen — verified at all. Those guardrails are frequently where the most damaging real-world bugs live, because an incorrectly-allowed transition (cancelling a shipped, already-charged order) has business and financial consequences, not just a cosmetic glitch.
Example
A minimal order state machine. Notice the function explicitly rejects an invalid transition rather than silently allowing it.
function transition(currentState, action) {
const rules = {
"pending:pay": "paid",
"pending:cancel": "cancelled",
"paid:ship": "shipped",
"paid:cancel": "cancelled",
};
const key = currentState + ":" + action;
return rules[key] ?? "rejected";
}
console.log(transition("pending", "pay")); // "paid" -- valid transition
console.log(transition("shipped", "cancel")); // "rejected" -- invalid transition, correctly blockedTry it yourself
Try one more valid transition and one more invalid one before running.
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Guided exercise
Guided exercise
A document can be 'draft', 'submitted', or 'approved'. 'submit' moves draft to submitted. 'approve' moves submitted to approved. Set expectedA = the result of submitting a draft, and expectedB = the result of trying to approve a draft directly (should be rejected).
Checks: correctly identifies the valid draft-to-submitted transition · correctly identifies the invalid draft-to-approved transition
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Stuck? Get a hint.
Independent exercise
Independent exercise
A subscription can be 'trial', 'active', or 'expired'. Implement subscriptionTransition(state, action) where: 'activate' moves trial to active; 'cancel' moves active to expired; any other combination should return 'rejected'. Also handle 'expired' having no valid outgoing actions at all.
Checks: trial + activate is handled correctly · active + cancel is handled correctly · expired has no valid outgoing transitions · an undefined transition (trial + cancel) is rejected
Code editor. Press Escape then Tab to leave the editor if keyboard focus becomes trapped. Press Control+Shift+M inside the editor to toggle Tab-key focus trapping.
Stuck? Get a hint.
Common mistakes
- Testing only the "happy path" sequence of valid state transitions and never verifying that invalid transitions are correctly rejected.
- Forgetting that a state can have zero valid outgoing transitions (a true "dead end" state like cancelled or expired), and failing to test that nothing can move out of it.
- Modeling states informally in your head instead of writing them down as an explicit table — informal models are where transitions silently get forgotten.
Knowledge check
Takeaway
State transition testing models a system's states and allowed transitions explicitly, then deliberately tests both what should succeed and — just as importantly — what should be correctly rejected.
Summary
This lesson covered modeling a feature as states and transitions, and designing test cases for both valid transitions and the guardrail-verifying invalid transitions that are easy to skip.
References
Your notes
Notes save automatically.
Finished this lesson?
Mark it complete to track your progress and schedule a future review.
AI tutor
The optional AI tutor isn't enabled in this deployment. All lessons, exercises, quizzes, and search work fully without it.