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:
| Source | Syntax | What It Changes |
|---|---|---|
| Scenario-level setup | with scenario: | Initial conditions |
| Event data/payload | with event: | Incoming event data |
| Event-level setup | with given: | Event-specific preconditions |
| Context variables | with context: | $variable values |
| Guard results | assume: ? guard = | Condition outcomes |
| Action behavior | assume: action = | Side effect results |
| Path divergence | after/receive/then | Event 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_errorOnly 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 appliedwith 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_errorwith 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_failedPath 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_failedThis means:
- Run happy path up to (and including)
:checkout - Instead of happy path's next event, receive
:payment_failed - 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_failedDivergence with Context
flow
third retry fails permanently:
after :checkout
receive :payment_failed from @payment
with context:
$retry_count is 2
= order is in #permanently_failedDivergence 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 calledSystem 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_requestComplete 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 #cancelledBest Practices
- Only specify deltas - Don't repeat happy path setup
- Name tests clearly - Describe what's different
- Use transition tests for guards - Test each branch
- Use scenario tests for flows - Test alternative paths
- Combine variations - Use multiple
withblocks together - Keep tests focused - One variation per test