Given-When-Then
EventFlow uses the Given-When-Then pattern for test scenarios. This familiar structure from BDD makes tests readable and well-organized.
The Pattern
Given: Initial state (setup)
When: Event occurs (action)
Then: Expected outcome (verification)In EventFlow:
flow
scenario: add item to cart
given: // Given: setup
cart is empty
$total is 0
on> :add_item from @customer // When: event
$items adds $item
$total increases by $price
expect: // Then: assertions
= $items contains $item
= $total equals 50Given Block
The given: block sets up the initial state. It's declarative - describe what should be true, not the steps to create it.
Scenario-Level Given
Applies to all events in the scenario:
flow
scenario: checkout process
given:
@customer is logged in as "[email protected]"
@customer has premium membership
cart contains:
| product | price |
| Laptop | 1200 |
| Mouse | 25 |
$total: number is 1225
$discount: number is 0
on> :checkout from @customer
...
on :payment_success from @payment
...Event-Level Given
Additional setup for a specific event:
flow
scenario: checkout with discount
given: // Scenario-level
@customer is logged in
cart has items
$total is 1200
on> :apply_discount from @customer
given: // Event-level (runs after scenario-level)
$discount_code is "SUMMER20"
$discount_percent is 20
? discount code is valid
$total decreases by ($total * $discount_percent / 100)
expect:
= $total equals 960Given Patterns
flow
given:
// Actor setup
@customer is logged in
@customer has verified email
@customer is premium member
// State setup
order is in #pending
cart is not empty
// Context setup
$total: number is 1200
$items: array contains "Laptop"
$created_at is yesterday
// Table data
cart contains:
| product | price | quantity |
| Laptop | 1200 | 1 |
| Mouse | 25 | 2 |
// Relationship setup
@customer has orders:
| order_id | status |
| ORD-001 | paid |
| ORD-002 | pending|When (Event Handlers)
The "When" is the event handler - the action being tested:
flow
on> :checkout from @customer // When: customer checks out
? cart is valid
order moves to #awaiting_payment
emit :payment_request to @paymentMultiple Events (Multi-Step When)
Some scenarios involve multiple events:
flow
scenario: complete purchase
given:
cart has items
on> :checkout from @customer // When step 1
order moves to #awaiting_payment
emit :payment_request to @payment
on :payment_success from @payment // When step 2
order moves to #paid
emit :ship_order to @warehouse
on :order_shipped from @warehouse // When step 3
order moves to #completed
expect:
= order is in #completedThen (Expect Block)
The expect: block verifies the outcome:
Event-Level Expect
Verify immediately after an event:
flow
on> :checkout from @customer
order moves to #awaiting_payment
expect: // Then: after checkout
= order is in #awaiting_payment
= :payment_request was emittedScenario-Level Expect
Verify after all events complete:
flow
scenario: full purchase flow
on> :checkout from @customer
...
on :payment_success from @payment
...
expect: // Then: after everything
= order is in #paid
= @customer received :confirmation
= $total equals $amount_chargedComplete Example
flow
machine: @order
scenario: customer applies discount during checkout
given:
@customer is logged in as "[email protected]"
@customer is premium member
cart contains:
| product | price |
| Laptop | 1000 |
$total: number is 1000
$discount_applied: boolean is false
on> :apply_discount from @customer
given:
$discount_code is "PREMIUM10"
? @customer is premium member
? discount code is valid
$discount becomes 10%
$total decreases by ($total * 0.10)
$discount_applied becomes true
expect:
= $discount_applied is true
= $total equals 900
on> :checkout from @customer
? cart is not empty
order moves to #awaiting_payment
emit :payment_request to @payment
with $total
expect:
= order is in #awaiting_payment
on :payment_success from @payment
order moves to #paid
emit :order_confirmed to @customer
expect:
= order is in #paid
= @customer received :order_confirmed
= $total equals 900
= $discount_applied is trueTesting Edge Cases
Negative Tests
flow
scenario: checkout fails with empty cart
given:
@customer is logged in
cart is empty
on> :checkout from @customer
? cart is empty
emit :checkout_error to @customer
expect:
= order is not in #awaiting_payment
= @customer received :checkout_errorBoundary Tests
flow
scenario: free shipping threshold
given:
$subtotal: number is 99
on> :calculate_shipping from @system
? $subtotal >= 100
$shipping becomes 0
otherwise
$shipping becomes 10
expect:
= $shipping equals 10 // Just under threshold
scenario: free shipping achieved
given:
$subtotal: number is 100
on> :calculate_shipping from @system
? $subtotal >= 100
$shipping becomes 0
otherwise
$shipping becomes 10
expect:
= $shipping equals 0 // At thresholdError Handling Tests
flow
scenario: handle payment failure
given:
cart has items
payment will fail
on> :checkout from @customer
order moves to #awaiting_payment
emit :payment_request to @payment
on :payment_failed from @payment
$retry_count increases by 1
? $retry_count < 3
order moves to #awaiting_payment
otherwise
order moves to #payment_failed
emit :order_cancelled to @customer
expect:
= order is in #payment_failed
= @customer received :order_cancelledBest Practices
Keep Given Minimal
flow
// Good - only what's needed
given:
cart has items
// Avoid - unnecessary setup
given:
@customer is logged in
@customer has email "[email protected]"
@customer has phone "555-1234"
@customer registered on "2023-01-01"
@customer has 10 previous orders
cart has items // Only this matters for the testMake Then Specific
flow
// Good - specific assertions
expect:
= order is in #paid
= $total equals 1200
= @customer received :confirmation
// Avoid - vague assertions
expect:
= everything worked
= order is validOne Concept Per Scenario
flow
// Good - focused
scenario: discount applies to total
// Tests discount calculation only
scenario: checkout validates cart
// Tests cart validation only
// Avoid - testing multiple things
scenario: full checkout with discounts and validation
// Too many things to debug if it fails