Skip to content

Session 3: Edge Cases

A focused session with just Jordan (QA) and Alex (Dev) to systematically discover edge cases and write failing tests.

Setting the Scene

Jordan and Alex join a quick call to discover edge cases and write the test file. This is a focused technical session - the failing tests will become Alex's implementation targets.

Before we bring the full team back, I want to map out all the edge cases and write failing tests for them.
Good idea. We follow TDD - write the test first, see it fail, then update the flow.
I've got a list from our discovery session: empty cart, not logged in, payment failures. Let's go through each one.

Creating the Test File

Let me create a separate test file. This keeps our main flow clean.
flow
// order.test.flow
test: @order

for scenario: checkout

  // We'll add test variations here
The test: keyword declares this is a test file for @order. And for scenario: targets the checkout scenario.
Exactly. Now let's add our first edge case: empty cart.

Edge Case 1: Empty Cart

When a customer clicks checkout with nothing in their cart, what should happen?
We should reject the checkout with a clear message.
Let me write the test. I'll use `with given:` to override the setup and `assume:` to control the guard behavior.
flow
test: @order

for scenario: checkout

  for :checkout:

    empty cart rejected:
      with given:
        @customer is logged in
        cart is empty
      assume:
        ? cart is not empty = false
      = @customer received :checkout_rejected
        with reason: "Please add items to your cart before checking out"
      = order is not in #awaiting_payment
Let me break down what you wrote: - `for :checkout:` - we're testing variations of the checkout event - `empty cart rejected:` - the test case name - `with given:` - override the scenario's given block - `assume:` - control how the guard evaluates - `=` assertions verify the expected outcomes
The `assume:` block is key. It lets us control guard results without mocking the actual implementation.
Let me run this test.
bash
$ eventflow test order.test.flow

@order / checkout

  :checkout variations
 empty cart rejected (8ms)
      Expected @customer to receive :checkout_rejected
      but no such event was emitted

1 failing
Good. The test fails because our flow doesn't handle empty carts yet. You'll implement this in Session 4.

Edge Case 2: Not Logged In

What about guest users? Should they be able to checkout?
Sarah said no for now. Let me write that test.
flow
    guest user redirected to login:
      with given:
        @customer is not logged in
        cart has items
      assume:
        ? @customer is logged in = false
      = @customer received :login_required
        with message: "Please log in to complete your purchase"
      = order is not in #awaiting_payment
Which check should happen first - login or cart?
Login first. If they're not logged in, we shouldn't tell them about cart issues. It's confusing.
Good call. That means we need nested guards - login check wraps the cart check.

Edge Case 3: Payment Failures

Payment failures are different. The checkout succeeds initially, but then the payment service responds with a failure.
So this tests a different event - :payment_failed instead of variations on :checkout.
Exactly. Let me write it with the `after` and `receive` keywords.
flow
  for :payment_failed:

    payment failure notifies customer:
      with context:
        order is in #awaiting_payment
        $order_id: "test-order-123"
        $retry_count: 0
      assume:
        @payment sends :payment_failed
          with reason: "Card declined"
      = @customer received :payment_failed_notification
        with $order_id
        with reason: "Payment could not be processed. Please try again."
      = $retry_count equals 1
      = order is in #payment_failed
You're using `with context:` instead of `with given:`. What's the difference?
`with context:` sets specific context values for the test. `with given:` overrides the declarative setup. Here we need the order to already be in #awaiting_payment.

Edge Case 4: Retry Limit Exceeded

What if payment fails multiple times? Should there be a retry limit?
Good thinking. Let's say 3 retries max.
flow
    retry limit exceeded:
      with context:
        order is in #payment_failed
        $order_id: "test-order-123"
        $retry_count: 3
      assume:
        @customer sends :retry_checkout
      = @customer received :max_retries_exceeded
        with message: "Maximum payment attempts reached. Please contact support."
      = order is in #cancelled
This tests what happens when $retry_count is already at 3 and they try again.
We'll need an OR guard for this - check if retries are under the limit OR if the user is an admin.
Admin override? Good idea. Let me add that test too.
flow
    admin can override retry limit:
      with context:
        order is in #payment_failed
        $order_id: "test-order-123"
        $retry_count: 5
      assume:
        @customer is admin = true
        @customer sends :retry_checkout
      = @payment received :payment_request
      = order is in #awaiting_payment
For the OR logic, we'll use `??` - the OR guard prefix.

Using observe: for Side Effects

I also want to verify that we're sending emails correctly. But I don't want to actually send them in tests.
Use `observe:` - it watches actions without changing behavior.
flow
  for :payment_success:

    confirmation email is sent:
      with context:
        order is in #awaiting_payment
        $order_id: "test-order-123"
      observe:
        send confirmation email
      assume:
        @payment sends :payment_success
      = send confirmation email was called
      = send confirmation email was called with $order_id
So `observe:` registers that we want to track the action, and then we can assert on it?
Yes. It's like spying in traditional testing - we watch without interfering.

The Complete Test File

Let me compile all our tests into the final file.
flow
// order.test.flow v1
test: @order

for scenario: checkout

  for :checkout:

    empty cart rejected:
      with given:
        @customer is logged in
        cart is empty
      assume:
        ? cart is not empty = false
      = @customer received :checkout_rejected
        with reason: "Please add items to your cart before checking out"
      = order is not in #awaiting_payment

    guest user redirected to login:
      with given:
        @customer is not logged in
        cart has items
      assume:
        ? @customer is logged in = false
      = @customer received :login_required
        with message: "Please log in to complete your purchase"
      = order is not in #awaiting_payment

  for :payment_success:

    confirmation email is sent:
      with context:
        order is in #awaiting_payment
        $order_id: "test-order-123"
      observe:
        send confirmation email
      assume:
        @payment sends :payment_success
      = send confirmation email was called
      = order is in #confirmed

  for :payment_failed:

    payment failure notifies customer:
      with context:
        order is in #awaiting_payment
        $order_id: "test-order-123"
        $retry_count: 0
      assume:
        @payment sends :payment_failed
          with reason: "Card declined"
      = @customer received :payment_failed_notification
      = $retry_count equals 1
      = order is in #payment_failed

  for :retry_checkout:

    retry after payment failure:
      with context:
        order is in #payment_failed
        $order_id: "test-order-123"
        $retry_count: 1
      assume:
        ? $retry_count is less than 3 = true
      = @payment received :payment_request
      = order is in #awaiting_payment

    retry limit exceeded:
      with context:
        order is in #payment_failed
        $order_id: "test-order-123"
        $retry_count: 3
      assume:
        ? $retry_count is less than 3 = false
        ? @customer is admin = false
      = @customer received :max_retries_exceeded
      = order is in #cancelled

    admin can override retry limit:
      with context:
        order is in #payment_failed
        $order_id: "test-order-123"
        $retry_count: 5
      assume:
        ? $retry_count is less than 3 = false
        ?? @customer is admin = true
      = @payment received :payment_request
      = order is in #awaiting_payment
Let me run all the tests to see the current state.
bash
$ eventflow test order.test.flow

@order / checkout

  :checkout variations
 empty cart rejected (8ms)
 guest user redirected to login (7ms)

  :payment_success variations
 confirmation email is sent (12ms)

  :payment_failed variations
 payment failure notifies customer (6ms)

  :retry_checkout variations
 retry after payment failure (5ms)
 retry limit exceeded (4ms)
 admin can override retry limit (4ms)

1 passing, 6 failing
6 failing tests. These are our implementation targets for the next session.
The happy path still works (confirmation email is sent). Now we have a clear roadmap.

Session Outcome

What We Created

  • Complete test file with 7 test cases
  • Tests for empty cart, guest users, payment failures, retries, admin override
  • Used observe: for side effect verification

Test Keywords Used

KeywordPurpose
test:Declare test file for a machine
for scenario:Target a specific scenario
for :event:Target an event handler
with given:Override scenario setup
with context:Set specific context values
assume:Control guard/action behavior
observe:Watch actions without changing behavior

Test Status

  • 1 passing (happy path email)
  • 6 failing (edge cases to implement)

Next Steps

Alex will implement all of this solo in Session 4:

  • Update flow with guards for login and cart validation
  • Add payment failure handling
  • Add retry logic with OR guards
  • Write PHP bindings for all guards, actions, and events
  • Turn all 6 failing tests green

Released under the MIT License.