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
| Keyword | Purpose |
|---|---|
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