Session 4b: TDD Implementation
Alex implements PHP bindings using the TDD Double Loop approach. The flow tests drive what needs to be built - error messages guide him to each binding that needs implementation.
The Double Loop Concept
EventFlow naturally supports a BDD/TDD Double Loop workflow:
- Outer Loop (BDD): Flow file defines WHAT - error messages tell you which binding is missing
- Inner Loop (TDD): Each binding follows Red-Green-Refactor
- Progress Check: After each binding, run
eventflow testto see progress
Starting Point: The First Outer Loop Fail
After deriving states in Session 4, Alex is ready to implement bindings.
Let me start by running the flow tests to see what's missing...
bash
$ eventflow test order.flow order.test.flow
@order / checkout
✗ checkout → awaiting_payment
Error: No binding found for guard 'cart is not empty'
Hint: Create a guard class with:
#[Guard('cart is not empty')]
class CartNotEmptyGuard extends GuardBehavior { ... }
Or run: eventflow make:guard "cart is not empty"
3 passing, 6 failing The error tells me exactly what I need to build first: a guard for "cart is not empty". But I'm doing TDD - so first I write the test!
Iteration 1: CartNotEmptyGuard
Step 1: Write the Binding Test (RED)
Let me write the test first. I'll think about what this guard needs to handle: null cart, empty cart, and cart with items.
php
<?php
declare(strict_types=1);
namespace Tests\Order\Guards;
use PHPUnit\Framework\TestCase;
use App\Order\Guards\CartNotEmptyGuard;
use EventFlow\Testing\Attributes\Tests;
#[Tests('cart is not empty')]
class CartNotEmptyGuardTest extends TestCase
{
private CartNotEmptyGuard $guard;
protected function setUp(): void
{
$this->guard = new CartNotEmptyGuard();
}
public function test_returns_false_when_cart_is_null(): void
{
$context = ['cart' => null];
$this->assertFalse(($this->guard)($context));
}
public function test_returns_false_when_cart_is_empty(): void
{
$cart = new \stdClass();
$cart->items = [];
$context = ['cart' => $cart];
$this->assertFalse(($this->guard)($context));
}
public function test_returns_true_when_cart_has_items(): void
{
$cart = new \stdClass();
$cart->items = [['id' => 1, 'name' => 'Laptop']];
$context = ['cart' => $cart];
$this->assertTrue(($this->guard)($context));
}
} Test is ready. Now let me run it - it should fail because the guard class doesn't exist yet.
bash
$ eventflow test:binding "cart is not empty"
Testing binding: 'cart is not empty'
Error: Class App\Order\Guards\CartNotEmptyGuard not found
Hint: Create a guard class with:
#[Guard('cart is not empty')]
class CartNotEmptyGuard extends GuardBehavior { ... }
0 passing, 1 failing RED! Exactly what I expected. Now I implement the minimum code to make it pass.
Step 2: Implement the Guard (GREEN)
php
<?php
declare(strict_types=1);
namespace App\Order\Guards;
use EventFlow\Attributes\Guard;
use EventFlow\Attributes\TestedBy;
use EventFlow\Behavior\GuardBehavior;
use Tests\Order\Guards\CartNotEmptyGuardTest;
#[Guard('cart is not empty')]
#[TestedBy(CartNotEmptyGuardTest::class)]
class CartNotEmptyGuard extends GuardBehavior
{
public function __invoke(array $context): bool
{
$cart = $context['cart'] ?? null;
if ($cart === null) {
return false;
}
return count($cart->items) > 0;
}
} Implementation done. Let me run the binding test again...
bash
$ eventflow test:binding "cart is not empty"
Testing binding: 'cart is not empty'
Binding: App\Order\Guards\CartNotEmptyGuard
✓ returns false when cart is null (2ms)
✓ returns false when cart is empty (1ms)
✓ returns true when cart has items (1ms)
3 passing (4ms) GREEN! All 3 tests pass. The implementation is simple enough - no refactoring needed. Let me check the outer loop...
Step 3: Check Outer Loop Progress
bash
$ eventflow test order.flow order.test.flow
@order / checkout
:checkout variations
✓ empty cart rejected (10ms) # NEW!
✗ checkout → awaiting_payment
Error: No binding found for guard '@customer is logged in'
4 passing, 5 failingProgress:4/9+1 scenario
One more scenario passes! The error now tells me the next binding I need: "@customer is logged in". Let me continue...
Iteration 2: CustomerLoggedInGuard
Step 1: Write the Test (RED)
For the login guard, I need to check: null customer, unauthenticated customer, and authenticated customer.
php
<?php
declare(strict_types=1);
namespace Tests\Order\Guards;
use PHPUnit\Framework\TestCase;
use App\Order\Guards\CustomerLoggedInGuard;
use App\Models\Customer;
use EventFlow\Testing\Attributes\Tests;
#[Tests('@customer is logged in')]
class CustomerLoggedInGuardTest extends TestCase
{
private CustomerLoggedInGuard $guard;
protected function setUp(): void
{
$this->guard = new CustomerLoggedInGuard();
}
public function test_returns_false_when_customer_is_null(): void
{
$context = ['customer' => null];
$this->assertFalse(($this->guard)($context));
}
public function test_returns_false_when_customer_is_not_authenticated(): void
{
$customer = $this->createMock(Customer::class);
$customer->method('isAuthenticated')->willReturn(false);
$context = ['customer' => $customer];
$this->assertFalse(($this->guard)($context));
}
public function test_returns_true_when_customer_is_authenticated(): void
{
$customer = $this->createMock(Customer::class);
$customer->method('isAuthenticated')->willReturn(true);
$context = ['customer' => $customer];
$this->assertTrue(($this->guard)($context));
}
}bash
$ eventflow test:binding "@customer is logged in"
Testing binding: '@customer is logged in'
Error: Class App\Order\Guards\CustomerLoggedInGuard not found
0 passing, 1 failing RED as expected. Now implement...
Step 2: Implement (GREEN)
php
<?php
declare(strict_types=1);
namespace App\Order\Guards;
use EventFlow\Attributes\Guard;
use EventFlow\Attributes\TestedBy;
use EventFlow\Behavior\GuardBehavior;
use Tests\Order\Guards\CustomerLoggedInGuardTest;
#[Guard('@customer is logged in')]
#[TestedBy(CustomerLoggedInGuardTest::class)]
class CustomerLoggedInGuard extends GuardBehavior
{
public function __invoke(array $context): bool
{
$customer = $context['customer'] ?? null;
if ($customer === null) {
return false;
}
return $customer->isAuthenticated();
}
}bash
$ eventflow test:binding "@customer is logged in"
Testing binding: '@customer is logged in'
Binding: App\Order\Guards\CustomerLoggedInGuard
✓ returns false when customer is null (2ms)
✓ returns false when customer is not authenticated (1ms)
✓ returns true when customer is authenticated (1ms)
3 passing (4ms) GREEN! Let me check the outer loop...
Step 3: Outer Loop Check
bash
$ eventflow test order.flow order.test.flow
@order / checkout
:checkout variations
✓ empty cart rejected (10ms)
✓ guest user redirected to login (9ms) # NEW!
✗ checkout → awaiting_payment
Error: No binding found for guard '$retry_count is less than 3'
5 passing, 4 failingProgress:5/9+1 scenario
Another scenario passes! Next up: retry limit guard...
Iteration 3: RetryLimitGuard
Step 1: Write the Test (RED)
The retry limit guard needs to check boundary conditions: 0, below limit, at limit, above limit, and missing value.
php
<?php
declare(strict_types=1);
namespace Tests\Order\Guards;
use PHPUnit\Framework\TestCase;
use App\Order\Guards\RetryLimitGuard;
use EventFlow\Testing\Attributes\Tests;
#[Tests('$retry_count is less than 3')]
class RetryLimitGuardTest extends TestCase
{
private RetryLimitGuard $guard;
protected function setUp(): void
{
$this->guard = new RetryLimitGuard();
}
public function test_returns_true_when_retry_count_is_zero(): void
{
$context = ['retry_count' => 0];
$this->assertTrue(($this->guard)($context));
}
public function test_returns_true_when_retry_count_is_below_limit(): void
{
$context = ['retry_count' => 2];
$this->assertTrue(($this->guard)($context));
}
public function test_returns_false_when_retry_count_equals_limit(): void
{
$context = ['retry_count' => 3];
$this->assertFalse(($this->guard)($context));
}
public function test_returns_false_when_retry_count_exceeds_limit(): void
{
$context = ['retry_count' => 5];
$this->assertFalse(($this->guard)($context));
}
public function test_defaults_to_zero_when_retry_count_not_set(): void
{
$context = [];
$this->assertTrue(($this->guard)($context));
}
}bash
$ eventflow test:binding "$retry_count is less than 3"
Testing binding: '$retry_count is less than 3'
Error: Class App\Order\Guards\RetryLimitGuard not found
0 passing, 1 failingStep 2: Implement (GREEN)
php
<?php
declare(strict_types=1);
namespace App\Order\Guards;
use EventFlow\Attributes\Guard;
use EventFlow\Attributes\TestedBy;
use EventFlow\Behavior\GuardBehavior;
use Tests\Order\Guards\RetryLimitGuardTest;
#[Guard('$retry_count is less than 3')]
#[TestedBy(RetryLimitGuardTest::class)]
class RetryLimitGuard extends GuardBehavior
{
private const MAX_RETRIES = 3;
public function __invoke(array $context): bool
{
$retryCount = $context['retry_count'] ?? 0;
return $retryCount < self::MAX_RETRIES;
}
}bash
$ eventflow test:binding "$retry_count is less than 3"
Testing binding: '$retry_count is less than 3'
Binding: App\Order\Guards\RetryLimitGuard
✓ returns true when retry count is zero (1ms)
✓ returns true when retry count is below limit (1ms)
✓ returns false when retry count equals limit (1ms)
✓ returns false when retry count exceeds limit (1ms)
✓ defaults to zero when retry count not set (1ms)
5 passing (5ms)Step 3: Outer Loop Check
bash
$ eventflow test order.flow order.test.flow
@order / checkout
:retry_checkout variations
✓ retry limit exceeded (10ms) # NEW!
✗ checkout → awaiting_payment
Error: No binding found for guard '@customer is admin'
6 passing, 3 failingProgress:6/9+1 scenario
Iteration 4: CustomerIsAdminGuard
The outer loop tells me I need an admin guard next. Same pattern: test first, then implement.
Step 1: Write the Test (RED)
php
<?php
declare(strict_types=1);
namespace Tests\Order\Guards;
use PHPUnit\Framework\TestCase;
use App\Order\Guards\CustomerIsAdminGuard;
use App\Models\Customer;
use EventFlow\Testing\Attributes\Tests;
#[Tests('@customer is admin')]
class CustomerIsAdminGuardTest extends TestCase
{
private CustomerIsAdminGuard $guard;
protected function setUp(): void
{
$this->guard = new CustomerIsAdminGuard();
}
public function test_returns_false_when_customer_is_null(): void
{
$context = ['customer' => null];
$this->assertFalse(($this->guard)($context));
}
public function test_returns_false_when_customer_is_not_admin(): void
{
$customer = $this->createMock(Customer::class);
$customer->method('hasRole')->with('admin')->willReturn(false);
$context = ['customer' => $customer];
$this->assertFalse(($this->guard)($context));
}
public function test_returns_true_when_customer_is_admin(): void
{
$customer = $this->createMock(Customer::class);
$customer->method('hasRole')->with('admin')->willReturn(true);
$context = ['customer' => $customer];
$this->assertTrue(($this->guard)($context));
}
}Step 2: Implement (GREEN)
php
<?php
declare(strict_types=1);
namespace App\Order\Guards;
use EventFlow\Attributes\Guard;
use EventFlow\Attributes\TestedBy;
use EventFlow\Behavior\GuardBehavior;
use Tests\Order\Guards\CustomerIsAdminGuardTest;
#[Guard('@customer is admin')]
#[TestedBy(CustomerIsAdminGuardTest::class)]
class CustomerIsAdminGuard extends GuardBehavior
{
public function __invoke(array $context): bool
{
$customer = $context['customer'] ?? null;
if ($customer === null) {
return false;
}
return $customer->hasRole('admin');
}
}bash
$ eventflow test:binding "@customer is admin"
Testing binding: '@customer is admin'
Binding: App\Order\Guards\CustomerIsAdminGuard
✓ returns false when customer is null (1ms)
✓ returns false when customer is not admin (1ms)
✓ returns true when customer is admin (1ms)
3 passing (3ms)Step 3: Outer Loop Check
bash
$ eventflow test order.flow order.test.flow
@order / checkout
:retry_checkout variations
✓ admin can override retry limit (11ms) # NEW!
7 passing, 2 failingProgress:7/9+1 scenario
Iteration 5: IncrementRetryCountAction
Now the outer loop is asking for an action: "$retry_count increases by 1". Let me write an action test.
Step 1: Write the Test (RED)
php
<?php
declare(strict_types=1);
namespace Tests\Order\Actions;
use PHPUnit\Framework\TestCase;
use App\Order\Actions\IncrementRetryCountAction;
use EventFlow\Testing\Attributes\Tests;
#[Tests('$retry_count increases by 1')]
class IncrementRetryCountActionTest extends TestCase
{
private IncrementRetryCountAction $action;
protected function setUp(): void
{
$this->action = new IncrementRetryCountAction();
}
public function test_increments_retry_count(): void
{
$context = ['retry_count' => 2];
$result = ($this->action)($context);
$this->assertEquals(['retry_count' => 3], $result);
}
public function test_defaults_to_one_when_not_set(): void
{
$context = [];
$result = ($this->action)($context);
$this->assertEquals(['retry_count' => 1], $result);
}
}Step 2: Implement (GREEN)
php
<?php
declare(strict_types=1);
namespace App\Order\Actions;
use EventFlow\Attributes\Action;
use EventFlow\Attributes\TestedBy;
use EventFlow\Behavior\ActionBehavior;
use Tests\Order\Actions\IncrementRetryCountActionTest;
#[Action('$retry_count increases by 1')]
#[TestedBy(IncrementRetryCountActionTest::class)]
class IncrementRetryCountAction extends ActionBehavior
{
public function __invoke(array $context): array
{
$retryCount = $context['retry_count'] ?? 0;
return ['retry_count' => $retryCount + 1];
}
}bash
$ eventflow test:binding "$retry_count increases by 1"
Testing binding: '$retry_count increases by 1'
Binding: App\Order\Actions\IncrementRetryCountAction
✓ increments retry count (1ms)
✓ defaults to one when not set (1ms)
2 passing (2ms)Step 3: Outer Loop Check
bash
$ eventflow test order.flow order.test.flow
@order / checkout
:retry_checkout variations
✓ retry after payment failure (16ms) # NEW!
8 passing, 1 failingProgress:8/9+1 scenario
Iteration 6: SendConfirmationEmailAction
Just one more scenario to go! The outer loop tells me I need "send confirmation email" action. This one has side effects, so I'll use mocking.
Step 1: Write the Test (RED)
php
<?php
declare(strict_types=1);
namespace Tests\Order\Actions;
use PHPUnit\Framework\TestCase;
use App\Order\Actions\SendConfirmationEmailAction;
use App\Services\EmailService;
use EventFlow\Testing\Attributes\Tests;
use Mockery;
#[Tests('send confirmation email')]
class SendConfirmationEmailActionTest extends TestCase
{
public function test_sends_confirmation_email(): void
{
$emailService = Mockery::mock(EmailService::class);
$emailService->shouldReceive('sendOrderConfirmation')
->once()
->with(
customerId: 'customer-123',
orderId: 'order-456',
total: 1200.00
);
$action = new SendConfirmationEmailAction($emailService);
$customer = new \stdClass();
$customer->id = 'customer-123';
$context = [
'customer' => $customer,
'order_id' => 'order-456',
'total' => 1200.00,
];
$result = ($action)($context);
$this->assertEquals([], $result); // No context changes
}
public function test_handles_missing_customer_gracefully(): void
{
$emailService = Mockery::mock(EmailService::class);
$emailService->shouldNotReceive('sendOrderConfirmation');
$action = new SendConfirmationEmailAction($emailService);
$context = [
'customer' => null,
'order_id' => 'order-456',
];
$result = ($action)($context);
$this->assertEquals([], $result);
}
protected function tearDown(): void
{
Mockery::close();
}
}Step 2: Implement (GREEN)
php
<?php
declare(strict_types=1);
namespace App\Order\Actions;
use EventFlow\Attributes\Action;
use EventFlow\Attributes\TestedBy;
use EventFlow\Behavior\ActionBehavior;
use App\Services\EmailService;
use Tests\Order\Actions\SendConfirmationEmailActionTest;
#[Action('send confirmation email')]
#[TestedBy(SendConfirmationEmailActionTest::class)]
class SendConfirmationEmailAction extends ActionBehavior
{
public function __construct(
private EmailService $emailService
) {}
public function __invoke(array $context): array
{
$customer = $context['customer'] ?? null;
if ($customer === null) {
return [];
}
$this->emailService->sendOrderConfirmation(
customerId: $customer->id,
orderId: $context['order_id'],
total: $context['total'] ?? 0.0
);
return [];
}
}bash
$ eventflow test:binding "send confirmation email"
Testing binding: 'send confirmation email'
Binding: App\Order\Actions\SendConfirmationEmailAction
✓ sends confirmation email (3ms)
✓ handles missing customer gracefully (1ms)
2 passing (4ms)Step 3: Final Outer Loop Check
bash
$ eventflow test order.flow order.test.flow
@order / checkout
happy path
✓ checkout → awaiting_payment (18ms)
✓ payment_success → confirmed (21ms)
:checkout variations
✓ empty cart rejected (12ms)
✓ guest user redirected to login (10ms)
:payment_success variations
✓ confirmation email is sent (14ms) # NEW!
:payment_failed variations
✓ payment failure notifies customer (11ms)
:retry_checkout variations
✓ retry after payment failure (16ms)
✓ retry limit exceeded (13ms)
✓ admin can override retry limit (12ms)
9 passingComplete!9/9All scenarios pass
Iteration 7: ValidEmailRule (Custom Validation)
The session 3 tests include validation edge cases. Now I need to implement a custom validation rule for email addresses.
Step 1: Write the Test (RED)
php
<?php
declare(strict_types=1);
namespace Tests\Order\Rules;
use PHPUnit\Framework\TestCase;
use App\Order\Rules\ValidEmailRule;
use EventFlow\Validation\Context;
use EventFlow\Testing\Attributes\Tests;
#[Tests('valid email')]
class ValidEmailRuleTest extends TestCase
{
private ValidEmailRule $rule;
protected function setUp(): void
{
$this->rule = new ValidEmailRule();
}
public function test_returns_false_for_null(): void
{
$this->assertFalse($this->rule->validate(null, new Context()));
}
public function test_returns_false_for_empty_string(): void
{
$this->assertFalse($this->rule->validate('', new Context()));
}
public function test_returns_false_for_invalid_email(): void
{
$this->assertFalse($this->rule->validate('not-an-email', new Context()));
$this->assertFalse($this->rule->validate('missing@domain', new Context()));
$this->assertFalse($this->rule->validate('@nodomain.com', new Context()));
}
public function test_returns_true_for_valid_email(): void
{
$this->assertTrue($this->rule->validate('user@example.com', new Context()));
$this->assertTrue($this->rule->validate('test.user@domain.org', new Context()));
}
}bash
$ eventflow test:binding "valid email"
Testing binding: 'valid email'
Error: Class App\Order\Rules\ValidEmailRule not found
0 passing, 1 failing RED! Now I implement the validation rule using the #[ValidationRule] attribute.
Step 2: Implement (GREEN)
php
<?php
declare(strict_types=1);
namespace App\Order\Rules;
use EventFlow\Attributes\ValidationRule;
use EventFlow\Attributes\TestedBy;
use EventFlow\Validation\Rule;
use EventFlow\Validation\Context;
use Tests\Order\Rules\ValidEmailRuleTest;
#[ValidationRule('valid email')]
#[TestedBy(ValidEmailRuleTest::class)]
class ValidEmailRule implements Rule
{
public function validate(mixed $value, Context $ctx): bool
{
if ($value === null || $value === '') {
return false;
}
return filter_var($value, FILTER_VALIDATE_EMAIL) !== false;
}
public function message(): string
{
return 'The {field} must be a valid email address.';
}
}bash
$ eventflow test:binding "valid email"
Testing binding: 'valid email'
Binding: App\Order\Rules\ValidEmailRule
✓ returns false for null (1ms)
✓ returns false for empty string (1ms)
✓ returns false for invalid email (1ms)
✓ returns true for valid email (1ms)
4 passing (4ms)Step 3: Outer Loop Check
bash
$ eventflow test order.flow order.test.flow
@order / checkout
:checkout variations
✓ invalid email rejected (8ms) # NEW!
10 passing, 0 failingProgress:10/10+1 scenario
Validation rules follow the same TDD pattern. The #[ValidationRule] attribute connects natural language validation to PHP code.
Final State
All 10 scenarios pass! Let me run the full test suite to see everything together...
bash
$ eventflow test order.flow order.test.flow --with-bindings
Loading bindings... 17 found
Flow Tests: order.flow + order.test.flow
────────────────────────────────────────
@order / checkout
happy path
✓ checkout → awaiting_payment (18ms)
✓ payment_success → confirmed (21ms)
:checkout variations
✓ empty cart rejected (12ms)
✓ guest user redirected to login (10ms)
✓ invalid email rejected (9ms)
✓ negative amount rejected (8ms)
:payment_success variations
✓ confirmation email is sent (14ms)
:payment_failed variations
✓ payment failure notifies customer (11ms)
:retry_checkout variations
✓ retry after payment failure (16ms)
✓ retry limit exceeded (13ms)
✓ admin can override retry limit (12ms)
✓ retry count constraint violated (10ms)
12 passing
Binding Tests:
────────────────────────────────────────
Guards
CartNotEmptyGuard ('cart is not empty')
✓ returns false when cart is null (2ms)
✓ returns false when cart is empty (1ms)
✓ returns true when cart has items (1ms)
CustomerLoggedInGuard ('@customer is logged in')
✓ returns false when customer is null (2ms)
✓ returns false when not authenticated (1ms)
✓ returns true when authenticated (1ms)
RetryLimitGuard ('$retry_count is less than 3')
✓ returns true when retry count is zero (1ms)
✓ returns true when below limit (1ms)
✓ returns false when at limit (1ms)
✓ returns false when above limit (1ms)
✓ defaults to zero (1ms)
CustomerIsAdminGuard ('@customer is admin')
✓ returns false when customer is null (1ms)
✓ returns false when not admin (1ms)
✓ returns true when admin (1ms)
Actions
IncrementRetryCountAction ('$retry_count increases by 1')
✓ increments retry count (1ms)
✓ defaults to one (1ms)
SendConfirmationEmailAction ('send confirmation email')
✓ sends confirmation email (3ms)
✓ handles missing customer (1ms)
Validation Rules
ValidEmailRule ('valid email')
✓ returns false for null (1ms)
✓ returns false for empty string (1ms)
✓ returns false for invalid email (1ms)
✓ returns true for valid email (1ms)
22 passing
────────────────────────────────────────
Total: 34 tests passing
Flow scenarios: 12
Binding units: 22Test Linking Report
bash
$ eventflow links:report order.flow
Binding-Test Links Report
─────────────────────────
Guards:
'cart is not empty'
Binding: App\Order\Guards\CartNotEmptyGuard
Tests: CartNotEmptyGuardTest (3 tests)
Status: ✓ Linked
'@customer is logged in'
Binding: App\Order\Guards\CustomerLoggedInGuard
Tests: CustomerLoggedInGuardTest (3 tests)
Status: ✓ Linked
'$retry_count is less than 3'
Binding: App\Order\Guards\RetryLimitGuard
Tests: RetryLimitGuardTest (5 tests)
Status: ✓ Linked
'@customer is admin'
Binding: App\Order\Guards\CustomerIsAdminGuard
Tests: CustomerIsAdminGuardTest (3 tests)
Status: ✓ Linked
Actions:
'$retry_count increases by 1'
Binding: App\Order\Actions\IncrementRetryCountAction
Tests: IncrementRetryCountActionTest (2 tests)
Status: ✓ Linked
'send confirmation email'
Binding: App\Order\Actions\SendConfirmationEmailAction
Tests: SendConfirmationEmailActionTest (2 tests)
Status: ✓ Linked
Validation Rules:
'valid email'
Binding: App\Order\Rules\ValidEmailRule
Tests: ValidEmailRuleTest (4 tests)
Status: ✓ Linked
Summary:
Bindings with tests: 7/7 (100%)
Total test links: 22
Missing coverage: 0 bindingsTDD Progress Visualization
Summary
The True TDD Flow
For each binding:
1. OUTER LOOP (FAIL)
$ eventflow test order.flow
→ Error tells you which binding is missing
2. WRITE BINDING TEST (RED)
$ eventflow test:binding "pattern"
→ Test fails because class doesn't exist
3. IMPLEMENT BINDING (GREEN)
$ eventflow test:binding "pattern"
→ All tests pass
4. OUTER LOOP CHECK
$ eventflow test order.flow
→ See progress, get next missing binding
5. REPEAT until all scenarios passKey Attributes
| Attribute | Location | Purpose |
|---|---|---|
#[Guard('...')] | Binding class | Connects guard to flow pattern |
#[Action('...')] | Binding class | Connects action to flow pattern |
#[ValidationRule('...')] | Binding class | Connects validation rule to flow pattern |
#[TestedBy(...)] | Binding class | Links binding to its tests |
#[Tests('...')] | Test class | Links test to binding pattern |
Test Coverage
| Type | Bindings | Unit Tests |
|---|---|---|
| Guards | 4 | 14 tests |
| Actions | 2 | 4 tests |
| Validation Rules | 1 | 4 tests |
| Total | 7 | 22 tests |
Benefits of This Approach
| Benefit | Description |
|---|---|
| Error-Driven | Outer loop errors guide what to build next |
| Incremental Progress | See scenarios pass one by one |
| Full Coverage | Every binding has linked tests |
| Traceability | eventflow links:report shows all connections |
| CI/CD Ready | eventflow links:validate --strict for pipelines |
Next: Session 5: Review - The team reviews the complete implementation together.