Skip to content

Delta-Based Test Variations

EventFlow tests are delta-based: they only specify what changes from the happy path. This keeps tests focused and maintainable.

Variation Sources

Tests can vary these elements from the happy path:

SourceSyntaxWhat It Changes
Scenario-level setupwith scenario:Initial conditions
Event data/payloadwith event:Incoming event data
Event-level setupwith given:Event-specific preconditions
Context variableswith context:$variable values
Guard resultsassume: ? guard =Condition outcomes
Action behaviorassume: action =Side effect results
Path divergenceafter/receive/thenEvent sequence

with scenario:

Override the scenario-level given: block:

flow
// Happy path given:
given:
  @customer is logged in
  cart has items
  $total: number is 100

// Test variation
guest cannot checkout:
  with scenario:
    @customer is not logged in
  = @customer received :login_required

empty cart rejected:
  with scenario:
    cart is empty
  = @customer received :empty_cart_error

Only specify what's different. Unspecified items remain from the happy path.

with event:

Override the event data/payload:

flow
// Happy path expects credit card
on> :checkout from @customer
  ? payment_method is "credit_card"
    process credit card
  ? payment_method is "bank_transfer"
    send bank instructions

// Test variations
bank transfer flow:
  with event:
    payment_method is "bank_transfer"
  = bank transfer instructions sent

large quantity order:
  with event:
    items: [{ product: "Widget", quantity: 1000 }]
  = bulk discount was applied

with given:

Override the event-level given: block:

flow
// Happy path event-level given
on> :checkout from @customer
  given:
    shipping_address is valid
    payment_method is verified

  ? shipping_address is valid
    calculate shipping
  otherwise
    emit :invalid_address to @customer

// Test variation
invalid shipping address:
  with given:
    shipping_address is not valid
  = @customer received :invalid_address_error

with context:

Override specific context variables:

flow
// Happy path context
$total: number is 100
$retry_count: number is 0

// Test variations
high value triggers fraud check:
  with context:
    $total is 5000
  = fraud check was triggered

retry scenario:
  with context:
    $retry_count is 2
    $last_error is "Timeout"
  = order is in #permanently_failed

Path Divergence

For scenario-level tests, specify where the path diverges from happy path:

Basic Divergence

flow
payment fails:
  after :checkout
  receive :payment_failed from @payment
  = order is in #payment_failed

This means:

  1. Run happy path up to (and including) :checkout
  2. Instead of happy path's next event, receive :payment_failed
  3. Verify assertions

Multiple Events After Divergence

flow
retry then succeed:
  after :checkout
  receive :payment_failed from @payment
  then :payment_success from @payment
  = order is in #paid
  = $retry_count equals 1

three failures then give up:
  after :checkout
  receive :payment_failed from @payment
  then :payment_failed from @payment
  then :payment_failed from @payment
  = order is in #permanently_failed

Divergence with Context

flow
third retry fails permanently:
  after :checkout
  receive :payment_failed from @payment
  with context:
    $retry_count is 2
  = order is in #permanently_failed

Divergence with Assumptions

flow
payment succeeds but email fails:
  after :checkout
  receive :payment_success from @payment
  assume:
    send confirmation email throws "SMTP error"
  observe:
    log email failure
  = order is in #paid
  = log email failure was called

System Test Variations

For machine systems, target specific machines:

flow
test: system checkout

  for scenario: complete purchase

    for :checkout to @order:
      guest rejected:
        with scenario:
          @customer is not logged in
        = @customer received :login_required

    for :payment_request to @payment:
      gateway down:
        assume:
          @payment.process throws "Gateway unavailable"
        = @order is in #payment_failed
        = @customer received :payment_error

    inventory out of stock:
      after :payment_success
      receive :out_of_stock from @inventory
      = @order is in #cancelled
      = @payment received :refund_request

Complete Example

flow
// order.test.flow
test: @order

  for scenario: complete checkout

    // Transition tests
    for :checkout:
      empty cart rejected:
        with scenario:
          cart is empty
        = @customer received :empty_cart_error

      gateway down queues order:
        assume:
          ? payment_gateway is available = false
        = order is in #queued

      payment declined:
        assume:
          ? payment successful = false
        = order is in #payment_failed

      fraud detected rejects order:
        with context:
          $total is 5000
        assume:
          ? fraud detected = true
        = order is in #fraud_rejected
        = @security received :fraud_alert

      boundary - fraud check at 1000:
        with context:
          $total is 1000
        observe:
          check fraud
        = check fraud was not called

      boundary - fraud check at 1001:
        with context:
          $total is 1001
        observe:
          check fraud
        = check fraud was called

    for :payment_failed:
      first failure retries:
        with context:
          $retry_count is 0
        = :retry_payment was emitted to @payment

      third failure gives up:
        with context:
          $retry_count is 2
        = order is in #permanently_failed

    // Scenario tests
    payment fails after checkout:
      after :checkout
      receive :payment_failed from @payment
      = order is in #permanently_failed

    payment fails then succeeds:
      after :checkout
      receive :payment_failed from @payment
      then :payment_success from @payment
      = order is in #paid
      = $retry_count equals 1

    cancel during payment:
      after :checkout
      receive :cancel from @customer
      = order is in #cancelled

Best Practices

  1. Only specify deltas - Don't repeat happy path setup
  2. Name tests clearly - Describe what's different
  3. Use transition tests for guards - Test each branch
  4. Use scenario tests for flows - Test alternative paths
  5. Combine variations - Use multiple with blocks together
  6. Keep tests focused - One variation per test

Released under the MIT License.