Skip to content

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 50

Given 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 960

Given 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 @payment

Multiple 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 #completed

Then (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 emitted

Scenario-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_charged

Complete 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 true

Testing 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_error

Boundary 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 threshold

Error 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_cancelled

Best 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 test

Make 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 valid

One 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

Released under the MIT License.