Skip to content

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 = Executable Specification (Sessions 1-3)6 Failing Scenariosfrom Session 3INNER LOOP (TDD)PHP Bindings: Unit test → Implementation → RefactorREDWrite testGREENImplementREFACTORClean upNext binding...Outer Loop Checkeventflow testProgress+1scenario
  • 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 test to 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 failing
Progress: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 failing
Progress: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 failing

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\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 failing
Progress: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 failing
Progress: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 failing
Progress: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 passing
Complete!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 failing
Progress: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: 22

Test 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 bindings

TDD Progress Visualization ​

TDD Implementation Progress1234567893 passing9 passingScenario passes after TDD iterationEach iteration: Outer loop fail → Write test → Implement → Outer loop check

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 pass

Key Attributes ​

AttributeLocationPurpose
#[Guard('...')]Binding classConnects guard to flow pattern
#[Action('...')]Binding classConnects action to flow pattern
#[ValidationRule('...')]Binding classConnects validation rule to flow pattern
#[TestedBy(...)]Binding classLinks binding to its tests
#[Tests('...')]Test classLinks test to binding pattern

Test Coverage ​

TypeBindingsUnit Tests
Guards414 tests
Actions24 tests
Validation Rules14 tests
Total722 tests

Benefits of This Approach ​

BenefitDescription
Error-DrivenOuter loop errors guide what to build next
Incremental ProgressSee scenarios pass one by one
Full CoverageEvery binding has linked tests
Traceabilityeventflow links:report shows all connections
CI/CD Readyeventflow links:validate --strict for pipelines

Next: Session 5: Review - The team reviews the complete implementation together.

Released under the MIT License.