Skip to content

Test Linking

EventFlow provides a bidirectional test linking system that connects your PHP bindings to their unit tests. This creates traceability, ensures coverage, and enables the eventflow test:binding command to run tests by pattern name.

Why Test Linking?

In a TDD workflow, you have two types of tests:

LoopTestsTool
Outer (BDD)Flow scenarioseventflow test order.flow
Inner (TDD)Binding unit testseventflow test:binding "pattern"

Test linking connects these two worlds:

┌─────────────────────────────────────────────────────────────────┐
│                    TEST LINKING ECOSYSTEM                       │
│                                                                 │
│  ┌─────────────────┐         ┌─────────────────┐               │
│  │  Flow Files     │         │  PHP Bindings   │               │
│  │                 │         │                 │               │
│  │  ? cart is not  │ ──────► │  #[Guard(...)]  │               │
│  │    empty        │         │  #[TestedBy()]  │               │
│  └─────────────────┘         └────────┬────────┘               │
│                                       │                        │
│                                       ▼                        │
│                              ┌─────────────────┐               │
│                              │  Test Classes   │               │
│                              │                 │               │
│                              │  #[Tests(...)]  │               │
│                              └─────────────────┘               │
└─────────────────────────────────────────────────────────────────┘

Bidirectional Linking

The linking is bidirectional - both the binding and the test declare their relationship:

1. Binding → Test (#[TestedBy])

Bindings declare which test classes verify them:

php
<?php

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;
        return $cart !== null && count($cart->items) > 0;
    }
}

2. Test → Binding (#[Tests])

Test classes declare which binding pattern they test:

php
<?php

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
{
    public function test_returns_false_when_cart_is_null(): void
    {
        $guard = new CartNotEmptyGuard();
        $this->assertFalse(($guard)(['cart' => null]));
    }

    public function test_returns_true_when_cart_has_items(): void
    {
        $guard = new CartNotEmptyGuard();
        $cart = new \stdClass();
        $cart->items = [['id' => 1]];
        $this->assertTrue(($guard)(['cart' => $cart]));
    }
}

N:M Linking

Links can be many-to-many:

One Test, Multiple Bindings

A test class can verify multiple related bindings:

php
#[Tests('cart is not empty')]
#[Tests('cart is valid')]
class CartGuardsIntegrationTest extends TestCase
{
    public function test_checkout_requires_valid_non_empty_cart(): void
    {
        // Tests both guards working together
    }
}

One Binding, Multiple Tests

A binding can be verified by multiple test classes:

php
#[Guard('cart is not empty')]
#[TestedBy(CartNotEmptyGuardTest::class)]       // Unit tests
#[TestedBy(CheckoutIntegrationTest::class)]     // Integration tests
class CartNotEmptyGuard extends GuardBehavior
{
    // ...
}

Running Linked Tests

Test a Single Binding

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)

Test All Bindings

bash
$ eventflow test:bindings

Testing all bindings...

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 true when authenticated (1ms)

Actions:
  IncrementRetryCountAction ('$retry_count increases by 1')
 increments retry count (1ms)
 defaults to one (1ms)

7 passing (9ms)

Test Bindings for a Flow File

bash
$ eventflow test:bindings order.flow

Testing bindings used in order.flow...

 'cart is not empty' - 3 tests
 '@customer is logged in' - 3 tests
 '$retry_count is less than 3' - 5 tests
 'send confirmation email' - 2 tests

13 passing

Combined Flow and Binding Tests

bash
$ eventflow test order.flow --with-bindings

Flow Tests: 9 passing
Binding Tests: 18 passing
────────────────────
Total: 27 tests passing

Placeholder System

During rapid TDD development, you may not want to specify exact class names. Use placeholders:

Using Placeholders

php
// Binding - use placeholder
#[Guard('cart is not empty')]
#[TestedBy('@cart-guard')]  // Placeholder starting with @
class CartNotEmptyGuard { }

// Test - same placeholder
#[Tests('@cart-guard')]
class CartNotEmptyGuardTest { }

Resolving Placeholders

When you're ready to finalize:

bash
$ eventflow links:pair

Resolving placeholders...

  @cart-guard
    Binding: CartNotEmptyGuard ('cart is not empty')
    Test: CartNotEmptyGuardTest
 Replacing placeholder with actual references

  @login-guard
    Binding: CustomerLoggedInGuard ('@customer is logged in')
    Test: CustomerLoggedInGuardTest
 Replacing placeholder with actual references

2 placeholders resolved

This updates the files to use proper class references:

php
// After resolution
#[Guard('cart is not empty')]
#[TestedBy(CartNotEmptyGuardTest::class)]
class CartNotEmptyGuard { }
bash
$ eventflow links:report

Binding-Test Links Report
─────────────────────────

Guards:
  'cart is not empty'
    Binding: App\Order\Guards\CartNotEmptyGuard
    Tests:
 CartNotEmptyGuardTest::test_returns_false_when_cart_is_null
 CartNotEmptyGuardTest::test_returns_false_when_cart_is_empty
 CartNotEmptyGuardTest::test_returns_true_when_cart_has_items
    Status: Linked (3 tests)

  '@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: None
    Status: No test coverage

Summary:
  Bindings with tests: 2/3 (67%)
  Total test links: 6
  Missing coverage: 1 binding

Report for Specific Flow

bash
$ eventflow links:report order.flow

# Shows only bindings used in order.flow

Basic Validation

bash
$ eventflow links:validate

Validating binding-test links...

 'cart is not empty' - properly linked
 '@customer is logged in' - properly linked
 '$retry_count is less than 3' - no test coverage

Validation passed with 1 warning

Strict Mode (CI/CD)

bash
$ eventflow links:validate --strict

Validating binding-test links...

Issues Found:

 Missing Test Coverage
    Guard '$retry_count is less than 3' has no linked tests
    Binding: App\Order\Guards\RetryLimitGuard

 Orphaned Test
    CartNotEmptyGuardTest::test_handles_edge_case
    Links to 'cart is not empty' but method not in #[TestedBy]

 Unresolved Placeholder
    @payment-guard not resolved to actual binding

Validation FAILED (strict mode)
  2 errors, 1 warning

Exit code: 1

Use in CI/CD pipelines:

yaml
# .github/workflows/test.yml
- name: Validate test links
  run: eventflow links:validate --strict

Automatically add missing #[TestedBy] attributes:

bash
$ eventflow links:sync

Syncing binding-test links...

  CartNotEmptyGuard
    Adding: #[TestedBy(CartNotEmptyGuardTest::class)]

  CustomerLoggedInGuard
    Already synced

  RetryLimitGuard
    No test found - skipping

1 binding updated

Configuration

Configure test linking in eventflow.php:

php
return [
    'bindings' => [
        'paths' => [
            'App\\Order\\Guards',
            'App\\Order\\Actions',
            'App\\Order\\Events',
        ],
    ],

    'tests' => [
        'paths' => [
            'Tests\\Order\\Guards',
            'Tests\\Order\\Actions',
        ],
        'framework' => 'phpunit',  // or 'pest'
    ],

    'links' => [
        // Fail CI if bindings lack tests
        'strict' => env('EVENTFLOW_STRICT_LINKS', false),

        // Allow @placeholder syntax
        'allow_placeholders' => true,

        // Auto-discover test files
        'auto_discover' => true,
    ],
];

Attribute Reference

#[TestedBy]

Used on binding classes to declare test coverage:

php
use EventFlow\Attributes\TestedBy;

#[TestedBy(MyTest::class)]                    // Link to test class
#[TestedBy(MyTest::class, 'test_method')]     // Link to specific method
#[TestedBy('@placeholder')]                    // Use placeholder
class MyGuard { }

Parameters:

  • $testClass (string|class-string): Test class name or placeholder
  • $testMethod (string|null): Optional specific test method

#[Tests]

Used on test classes to declare what binding they test:

php
use EventFlow\Testing\Attributes\Tests;

#[Tests('cart is not empty')]           // Guard pattern
#[Tests('@customer is logged in')]      // Actor guard pattern
#[Tests('send confirmation email')]     // Action pattern
#[Tests('@cart-placeholder')]           // Placeholder
class MyTest { }

Parameters:

  • $bindingPattern (string): The exact pattern from the flow file or a placeholder

Best Practices

Add links as you write tests:

php
// 1. Write test with #[Tests]
#[Tests('cart is not empty')]
class CartNotEmptyGuardTest { }

// 2. Implement binding with #[TestedBy]
#[Guard('cart is not empty')]
#[TestedBy(CartNotEmptyGuardTest::class)]
class CartNotEmptyGuard { }

2. Use Strict Mode in CI

yaml
# Ensure all bindings have tests before merge
- run: eventflow links:validate --strict
bash
# Weekly check for coverage gaps
eventflow links:report --format=json > coverage.json

4. Resolve Placeholders Before PR

bash
# Before creating a pull request
eventflow links:pair
eventflow links:validate --strict

Integration with IDE

PHPStorm

Navigate from binding to test:

  1. Click on #[TestedBy(MyTest::class)]
  2. Ctrl+Click to jump to test class

Navigate from test to binding:

  1. Click on #[Tests('pattern')]
  2. Use "Find Usages" to find the binding

VS Code

Install EventFlow extension for:

  • Syntax highlighting for #[Tests] patterns
  • Quick navigation between binding and test
  • Inline coverage indicators

Related:

Released under the MIT License.