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.