All posts
MermaidState DiagramsTestingBug ReproductionQA Documentation

State diagrams for test scenarios: mapping bug reproduction paths

6 min readThe MermaidCreator team

When a bug lands in your issue tracker, the first thing a developer asks is "how do I reproduce it?" The answer is usually a wall of text: "log in as role X, create an object A, wait for state transition B, then try to do action C." State diagrams excel at making this precise and visual—they show which states matter, what transitions are valid, and which paths lead to the bug.

State diagrams also work as living test documentation. Every edge in your diagram is a test case; every path is a scenario. This keeps QA artifacts close to the code they test, making them easy to maintain as the product evolves.

Why state diagrams work for testing

A state diagram is a node-and-edge graph where:

  • Nodes are discrete states your system can be in
  • Edges are transitions triggered by actions (user input, events, timeouts)
  • Paths are sequences of transitions—test cases

This maps naturally to testing. A test script is just a path through your state diagram. A regression is a path that should exist but doesn't. An edge case is often a transition you forgot to document.

Here's a simple example: a user authentication flow.

stateDiagram-v2
    [*] --> Unauthenticated
    
    Unauthenticated --> EmailSent: submit email
    EmailSent --> Verified: click email link
    EmailSent --> EmailSent: resend link (< 10 sec) - blocked
    EmailSent --> Unauthenticated: abandon after 24h
    
    Verified --> Authenticated: session created
    Authenticated --> Unauthenticated: logout
    Authenticated --> Unauthenticated: session expired
    
    note right of EmailSent
        Rate-limited to 1 per 10s
        Expires after 24h
    end note

Every path in this diagram is a test case:

  1. ✅ Login happy path: Unauthenticated → EmailSent → Verified → Authenticated
  2. ✅ Logout: Authenticated → Unauthenticated
  3. ✅ Session expires: Authenticated → Unauthenticated
  4. ✅ Resend link blocked: EmailSent → EmailSent (should error)
  5. ✅ Link expires: EmailSent → Unauthenticated (after 24h, link is invalid)

Real-world example: order checkout flow

Here's a more complex scenario—an e-commerce checkout. Notice how the diagram captures edge cases that could become bugs:

stateDiagram-v2
    [*] --> Cart
    
    Cart --> PaymentInfo: proceed to checkout
    Cart --> [*]: abandon
    
    PaymentInfo --> PaymentProcessing: submit payment
    PaymentInfo --> Cart: edit cart
    
    PaymentProcessing --> PaymentFailed: card declined / timeout
    PaymentProcessing --> AwaitingCapture: payment authorized
    
    PaymentFailed --> PaymentInfo: retry
    PaymentFailed --> [*]: abandon
    
    AwaitingCapture --> OrderConfirmed: capture succeeds
    AwaitingCapture --> PaymentCancelled: auto-cancel (24h timeout)
    
    PaymentCancelled --> PaymentInfo: retry payment
    
    OrderConfirmed --> Shipped: fulfillment picks items
    OrderConfirmed --> OrderCancelled: user cancels (< 2h)
    
    OrderCancelled --> PaymentRefunding: initiate refund
    PaymentRefunding --> PaymentRefunded: refund processed
    PaymentRefunded --> [*]
    
    Shipped --> Delivered
    Delivered --> [*]
    
    note right of AwaitingCapture
        Payment authorized but not captured.
        Auto-cancel after 24h to release funds.
    end note
    
    note right of OrderCancelled
        Only allowed if order hasn't been
        picked/packed yet (~2h window).
    end note

This diagram surfaces edge cases that might not be obvious:

  • Orphaned auth-only captures: if capture fails and auto-cancel doesn't trigger, funds are held indefinitely
  • Race condition: user cancels while fulfillment picks—which wins?
  • Refund timeout: what if the refund API hangs?

Each becomes a test case: write a scenario that triggers it and verify the system handles it correctly.

Mapping test scenarios to state paths

Every test scenario is a path (or set of paths) through the diagram. Here's how to use them together:

Test NamePathExpected
Happy pathCart → PaymentInfo → PaymentProcessing → OrderConfirmed → Shipped → DeliveredOrder placed and fulfilled
Card declinedCart → PaymentInfo → PaymentProcessing → PaymentFailed → PaymentInfoError shown; user can retry
Early cancelCart → PaymentInfo → PaymentProcessing → AwaitingCapture → OrderCancelled → PaymentRefunding → PaymentRefundedOrder and payment reversed
Late cancelCart → ... → ShippedRejection: order already shipped
Capture timeoutCart → ... → AwaitingCapture (24h later) → PaymentCancelled → PaymentInfoPayment released; user can retry

This mapping makes it clear which scenarios you've covered and which you've missed.

Documenting bug reproduction with state diagrams

When a bug is reported, draw the exact state path that triggered it:

Bug: "Refund processed but user wasn't notified"

stateDiagram-v2
    OrderConfirmed --> OrderCancelled: user clicks cancel
    OrderCancelled --> PaymentRefunding: refund initiated
    PaymentRefunding --> PaymentRefunded: refund succeeds
    PaymentRefunded --> [*]
    
    note right of PaymentRefunded
        BUG: No email sent to user.
        Should trigger "refund_completed" email.
    end note

The diagram makes the bug scenario reproducible and pinpoints exactly where the system failed to act.

Complex workflows: substates and guards

For more intricate flows, nest states or add guards (conditions on transitions):

stateDiagram-v2
    [*] --> Processing
    
    Processing --> PaymentProcessing: payment_handler triggers
    Processing --> ComplianceReview: high_risk_score
    
    state PaymentProcessing {
        [*] --> CardValidator
        CardValidator --> FraudCheck: card valid
        FraudCheck --> Approved: score < threshold
        FraudCheck --> Declined: score >= threshold
        Approved --> [*]
        Declined --> [*]
    }
    
    state ComplianceReview {
        [*] --> ManualReview
        ManualReview --> Approved: reviewer approves
        ManualReview --> Declined: reviewer rejects
        Approved --> [*]
        Declined --> [*]
    }
    
    PaymentProcessing --> Finalized: approved
    PaymentProcessing --> Rejected: declined
    ComplianceReview --> Finalized: approved
    ComplianceReview --> Rejected: declined
    
    Finalized --> [*]
    Rejected --> [*]

This models payment processing that branches into fraud checks and compliance review. Each substate is its own mini-workflow—perfect for defining test coverage.

Tips for state diagrams in QA

  1. One diagram per subsystem. Don't try to fit the entire product into one diagram. Keep it focused: payments, auth, order fulfillment, notifications.

  2. Label transitions clearly. "proceed" is vague; "submit_payment_form" is testable. Use verb + object.

  3. Add notes for constraints. If a transition has a timeout, rate limit, or permission check, document it. This becomes part of the test specification.

  4. Keep it up-to-date. When you add a feature or fix a bug, update the diagram. It's living documentation.

  5. Use it in code reviews. "Did you handle the timeout from state X to state Y?" Point to the diagram. Makes reviews faster.

  6. Link it from your issue tracker. Paste the diagram (or link to it) in bug reports and feature tickets. It gives context faster than text.

FAQ

Should I diagram every state in my system?
No—only the ones that matter for behavior. Internal implementation states that don't affect visible behavior can be implicit.

Can I use state diagrams for load testing or performance tests?
State diagrams focus on which transitions happen, not how long they take. Use them for functional test coverage; track performance separately in your metrics/monitoring system.

How do I handle async operations (webhooks, background jobs)?
Model them as separate states. "AwaitingCapture" is a state where the system waits for an external event. The diagram stays the same; you just note that transitions may be delayed or timeout.

What if a state has 20 possible transitions?
It's a smell that the state is doing too much. Consider splitting it into substates or breaking the feature into smaller workflows.

Document your next test scenario visually in the MermaidCreator editor—create a state diagram that captures every path through your feature, then use it to drive your QA checklist.

Related posts