Symfony Event Dispatcher mit PHPUnit testen: Von Mocks zu echten Tests

Von Roland Golla
0 Kommentar
Surreales PHPUnit Testbild: Lupe prüft Code, Test-Schmetterlinge in Grün/Rot

„Warum schlägt mein Test fehl, aber in Production funktioniert alles?“ – diese Frage begleitet uns seit über 15 Jahren in der Softwarequalität. Das Problem: Übermäßiges Mocking von Event Dispatchern verschleiert echte Fehler und führt zu falschen Sicherheitsgefühlen. Nach jahrelanger Erfahrung in Open Source und Remote Consulting zeigen wir euch heute, wie ihr Event-driven Code richtig testet – ohne in die Mock-Falle zu tappen.

Symfony Testing professionell – Externe Experten sofort verfügbar

Event Dispatcher testen ist komplex, euer Team hat keine Kapazität dafür? Als externe Agentur übernehmen wir die komplette Test-Implementierung noch am selben Tag. Unser TESTIFY SaaS-Service entlastet euer Team, liefert fertige PHPUnit-Tests und schult eure Entwickler parallel. Sofortige Unterstützung statt wochenlanger Eigenentwicklung.

Symfony Tests outsourcen →

Warum Testing von Events eure Code-Qualität fundamental verbessert

Richtig getestete Event-Systeme sind essentiell für:

  • Vertrauen in Refactorings: Tests brechen wenn Verhalten sich ändert
  • Dokumentation durch Tests: Code-Beispiele die wirklich funktionieren
  • Regression Prevention: Bugs die einmal gefixt wurden, bleiben gefixt
  • Team-Onboarding: Neue Developer verstehen Event-Flows durch Tests
  • Deployment-Sicherheit: CI/CD Pipeline catcht Fehler vor Production

Das Team von Never Code Alone hat in unzähligen Projekten erlebt, wie schlechte Event-Tests zu Production-Bugs führen, während gute Tests die Entwicklungsgeschwindigkeit drastisch erhöhen.

Die 10 häufigsten Fragen zum Testen von Symfony Events – direkt beantwortet

1. Wie teste ich einen Event Listener isoliert?

Der Schlüssel ist: Erstellt echte Event-Objekte statt alles zu mocken.

Best Practice: Real Events mit Mock-Dependencies

<?php

declare(strict_types=1);

namespace AppTestsEventListener;

use AppEventListenerOrderCompletedListener;
use AppEventOrderCompletedEvent;
use AppServiceInvoiceGenerator;
use PHPUnitFrameworkTestCase;

class OrderCompletedListenerTest extends TestCase
{
    public function testGeneratesInvoiceWhenOrderCompleted(): void
    {
        // Mock nur die externe Dependency
        $invoiceGenerator = $this->createMock(InvoiceGenerator::class);
        $invoiceGenerator
            ->expects($this->once())
            ->method('generate')
            ->with(
                $this->equalTo(12345),
                $this->equalTo(99.99)
            )
            ->willReturn('invoice-abc123.pdf');

        // Real Event Object
        $event = new OrderCompletedEvent(
            orderId: 12345,
            amount: 99.99
        );

        // Test den Listener
        $listener = new OrderCompletedListener($invoiceGenerator);
        $listener->__invoke($event);

        // Optional: Assert Event wurde nicht modifiziert
        $this->assertEquals(12345, $event->getOrderId());
        $this->assertEquals(99.99, $event->getAmount());
    }

    public function testSkipsSmallOrders(): void
    {
        $invoiceGenerator = $this->createMock(InvoiceGenerator::class);
        $invoiceGenerator
            ->expects($this->never())
            ->method('generate');

        $event = new OrderCompletedEvent(
            orderId: 999,
            amount: 5.00  // Unter Minimum
        );

        $listener = new OrderCompletedListener($invoiceGenerator);
        $listener->__invoke($event);
    }
}

Anti-Pattern: Alles mocken

// ❌ NICHT so!
public function testBad(): void
{
    $event = $this->createMock(OrderCompletedEvent::class);
    $event->method('getOrderId')->willReturn(123);
    // Problem: Testet nur Mock-Interaktion, nicht echtes Verhalten
}

Praxis-Tipp: Mockt nur externe Abhängigkeiten (DB, APIs, Services). Events, Value Objects und DTOs sollten immer echt sein!

2. Warum sind echte Dispatcher besser als Mocks?

Mocks täuschen euch – echte Dispatcher zeigen die Wahrheit.

Das Problem mit gemockten Dispatchern:

// ❌ Anti-Pattern: Gemockter Dispatcher
public function testWithMock(): void
{
    $dispatcher = $this->createMock(EventDispatcherInterface::class);
    $dispatcher
        ->expects($this->once())
        ->method('dispatch')
        ->with($this->isInstanceOf(OrderCompletedEvent::class));

    $service = new OrderService($dispatcher);
    $service->completeOrder(123, 99.99);

    // Problem 1: Test bricht nicht wenn dispatch() entfernt wird
    // Problem 2: Test bricht nicht wenn falscher Event-Typ dispatched wird
    // Problem 3: Listener werden nie wirklich ausgeführt
}

Best Practice: Echter Dispatcher mit Test-Listenern:

<?php

declare(strict_types=1);

namespace AppTestsService;

use AppEventOrderCompletedEvent;
use AppServiceOrderService;
use PHPUnitFrameworkTestCase;
use SymfonyComponentEventDispatcherEventDispatcher;

class OrderServiceTest extends TestCase
{
    public function testDispatchesOrderCompletedEvent(): void
    {
        // Echter Dispatcher
        $dispatcher = new EventDispatcher();

        // Test-Listener der Events sammelt
        $capturedEvents = [];
        $dispatcher->addListener(
            OrderCompletedEvent::class,
            function (OrderCompletedEvent $event) use (&$capturedEvents) {
                $capturedEvents[] = $event;
            }
        );

        $service = new OrderService($dispatcher);
        $service->completeOrder(123, 99.99);

        // Assertions auf echte Events
        $this->assertCount(1, $capturedEvents);
        $this->assertInstanceOf(OrderCompletedEvent::class, $capturedEvents[0]);
        $this->assertEquals(123, $capturedEvents[0]->getOrderId());
        $this->assertEquals(99.99, $capturedEvents[0]->getAmount());
    }

    public function testMultipleListenersReceiveEvent(): void
    {
        $dispatcher = new EventDispatcher();

        $listener1Called = false;
        $listener2Called = false;

        $dispatcher->addListener(
            OrderCompletedEvent::class,
            function () use (&$listener1Called) {
                $listener1Called = true;
            },
            10  // Priority
        );

        $dispatcher->addListener(
            OrderCompletedEvent::class,
            function () use (&$listener2Called) {
                $listener2Called = true;
            },
            5  // Niedrigere Priority
        );

        $service = new OrderService($dispatcher);
        $service->completeOrder(123, 99.99);

        $this->assertTrue($listener1Called);
        $this->assertTrue($listener2Called);
    }
}

Architektur-Vorteil: Tests brechen sofort wenn ihr den dispatch()-Call entfernt oder die Event-Struktur ändert!

3. Wie teste ich ob ein Service Events dispatched?

Kombiniert echte Dispatcher mit Event-Capturing-Pattern.

Event-Capturing Test Helper:

<?php

declare(strict_types=1);

namespace AppTestsHelper;

use SymfonyComponentEventDispatcherEventDispatcher;

class EventCapturingDispatcher extends EventDispatcher
{
    private array $capturedEvents = [];

    public function dispatch(object $event, string $eventName = null): object
    {
        // Event speichern
        $this->capturedEvents[] = [
            'event' => $event,
            'name' => $eventName ?? get_class($event)
        ];

        // Normal dispatchen
        return parent::dispatch($event, $eventName);
    }

    public function getCapturedEvents(string $eventClass = null): array
    {
        if ($eventClass === null) {
            return $this->capturedEvents;
        }

        return array_filter(
            $this->capturedEvents,
            fn($captured) => $captured['event'] instanceof $eventClass
        );
    }

    public function getLastEvent(string $eventClass = null): ?object
    {
        $events = $this->getCapturedEvents($eventClass);
        return !empty($events) ? end($events)['event'] : null;
    }

    public function assertEventDispatched(string $eventClass): void
    {
        $count = count($this->getCapturedEvents($eventClass));
        if ($count === 0) {
            throw new RuntimeException(
                sprintf('Event %s was not dispatched', $eventClass)
            );
        }
    }

    public function reset(): void
    {
        $this->capturedEvents = [];
    }
}

Verwendung im Test:

class PaymentServiceTest extends TestCase
{
    private EventCapturingDispatcher $dispatcher;

    protected function setUp(): void
    {
        $this->dispatcher = new EventCapturingDispatcher();
    }

    public function testPaymentDispatchesCorrectEvents(): void
    {
        $service = new PaymentService($this->dispatcher, $gateway);
        $service->processPayment($order);

        // Assert Events wurden dispatched
        $this->dispatcher->assertEventDispatched(PaymentStartedEvent::class);
        $this->dispatcher->assertEventDispatched(PaymentCompletedEvent::class);

        // Assert Event-Reihenfolge
        $events = $this->dispatcher->getCapturedEvents();
        $this->assertInstanceOf(PaymentStartedEvent::class, $events[0]['event']);
        $this->assertInstanceOf(PaymentCompletedEvent::class, $events[1]['event']);

        // Assert Event-Daten
        $completedEvent = $this->dispatcher->getLastEvent(PaymentCompletedEvent::class);
        $this->assertEquals('success', $completedEvent->getStatus());
    }
}

Consulting-Tipp: Dieser Helper ist wiederverwendbar in allen euren Projekten – investiert einmal, nutzt jahrelang!

4. Wie teste ich Event Subscriber mit mehreren Events?

Subscriber sind komplexer – testet jede Methode einzeln und die Integration.

Subscriber unter Test:

namespace AppEventSubscriber;

use AppEventOrderCompletedEvent;
use AppEventPaymentFailedEvent;
use SymfonyComponentEventDispatcherEventSubscriberInterface;

class OrderNotificationSubscriber implements EventSubscriberInterface
{
    public function __construct(
        private MailerInterface $mailer,
        private LoggerInterface $logger
    ) {
    }

    public static function getSubscribedEvents(): array
    {
        return [
            OrderCompletedEvent::class => ['onOrderCompleted', 10],
            PaymentFailedEvent::class => ['onPaymentFailed', 5],
        ];
    }

    public function onOrderCompleted(OrderCompletedEvent $event): void
    {
        $this->mailer->send(
            to: $event->getCustomerEmail(),
            subject: 'Order Completed',
            body: sprintf('Order #%s completed', $event->getOrderId())
        );

        $this->logger->info('Order completed notification sent', [
            'order_id' => $event->getOrderId()
        ]);
    }

    public function onPaymentFailed(PaymentFailedEvent $event): void
    {
        $this->logger->error('Payment failed', [
            'order_id' => $event->getOrderId(),
            'reason' => $event->getReason()
        ]);
    }
}

Umfassende Test-Suite:

<?php

declare(strict_types=1);

namespace AppTestsEventSubscriber;

use AppEventOrderCompletedEvent;
use AppEventPaymentFailedEvent;
use AppEventSubscriberOrderNotificationSubscriber;
use PHPUnitFrameworkTestCase;
use PsrLogLoggerInterface;
use SymfonyComponentEventDispatcherEventDispatcher;
use SymfonyComponentMailerMailerInterface;

class OrderNotificationSubscriberTest extends TestCase
{
    private MailerInterface $mailer;
    private LoggerInterface $logger;

    protected function setUp(): void
    {
        $this->mailer = $this->createMock(MailerInterface::class);
        $this->logger = $this->createMock(LoggerInterface::class);
    }

    public function testSubscribesToCorrectEvents(): void
    {
        $subscribedEvents = OrderNotificationSubscriber::getSubscribedEvents();

        $this->assertArrayHasKey(OrderCompletedEvent::class, $subscribedEvents);
        $this->assertArrayHasKey(PaymentFailedEvent::class, $subscribedEvents);

        // Assert Priorities
        $this->assertEquals(10, $subscribedEvents[OrderCompletedEvent::class][1]);
        $this->assertEquals(5, $subscribedEvents[PaymentFailedEvent::class][1]);
    }

    public function testSendsEmailOnOrderCompleted(): void
    {
        $this->mailer
            ->expects($this->once())
            ->method('send')
            ->with(
                $this->equalTo('customer@example.com'),
                $this->equalTo('Order Completed'),
                $this->stringContains('Order #123')
            );

        $this->logger
            ->expects($this->once())
            ->method('info')
            ->with(
                $this->equalTo('Order completed notification sent'),
                $this->arrayHasKey('order_id')
            );

        $event = new OrderCompletedEvent(
            orderId: 123,
            customerEmail: 'customer@example.com'
        );

        $subscriber = new OrderNotificationSubscriber($this->mailer, $this->logger);
        $subscriber->onOrderCompleted($event);
    }

    public function testLogsPaymentFailure(): void
    {
        $this->logger
            ->expects($this->once())
            ->method('error')
            ->with(
                $this->equalTo('Payment failed'),
                $this->callback(function (array $context) {
                    return $context['order_id'] === 456 
                        && $context['reason'] === 'Insufficient funds';
                })
            );

        $event = new PaymentFailedEvent(
            orderId: 456,
            reason: 'Insufficient funds'
        );

        $subscriber = new OrderNotificationSubscriber($this->mailer, $this->logger);
        $subscriber->onPaymentFailed($event);
    }

    public function testIntegrationWithRealDispatcher(): void
    {
        $dispatcher = new EventDispatcher();
        $subscriber = new OrderNotificationSubscriber($this->mailer, $this->logger);

        $dispatcher->addSubscriber($subscriber);

        $this->mailer->expects($this->once())->method('send');

        $event = new OrderCompletedEvent(123, 'test@example.com');
        $dispatcher->dispatch($event);
    }
}

Best Practice: Unit-Tests für jede Methode + ein Integration-Test der die Subscriber-Registration testet.

5. Wie erstelle ich Test-Fixtures für komplexe Events?

Data Builders und Factory-Pattern sind eure Freunde.

Event Builder Pattern:

<?php

declare(strict_types=1);

namespace AppTestsBuilder;

use AppEventOrderCompletedEvent;

class OrderCompletedEventBuilder
{
    private int $orderId = 1;
    private float $amount = 100.00;
    private string $customerEmail = 'test@example.com';
    private array $items = [];
    private ?DateTimeInterface $completedAt = null;

    public static function create(): self
    {
        return new self();
    }

    public function withOrderId(int $orderId): self
    {
        $this->orderId = $orderId;
        return $this;
    }

    public function withAmount(float $amount): self
    {
        $this->amount = $amount;
        return $this;
    }

    public function withCustomerEmail(string $email): self
    {
        $this->customerEmail = $email;
        return $this;
    }

    public function withItems(array $items): self
    {
        $this->items = $items;
        return $this;
    }

    public function completedNow(): self
    {
        $this->completedAt = new DateTimeImmutable();
        return $this;
    }

    public function completedAt(DateTimeInterface $dateTime): self
    {
        $this->completedAt = $dateTime;
        return $this;
    }

    public function build(): OrderCompletedEvent
    {
        return new OrderCompletedEvent(
            orderId: $this->orderId,
            amount: $this->amount,
            customerEmail: $this->customerEmail,
            items: $this->items,
            completedAt: $this->completedAt ?? new DateTimeImmutable()
        );
    }
}

Verwendung in Tests:

class OrderServiceTest extends TestCase
{
    public function testHighValueOrders(): void
    {
        $event = OrderCompletedEventBuilder::create()
            ->withOrderId(999)
            ->withAmount(5000.00)
            ->withCustomerEmail('vip@example.com')
            ->completedNow()
            ->build();

        $service = new OrderService($this->dispatcher);
        $service->handleCompletedOrder($event);

        // Assertions...
    }

    public function testSmallOrders(): void
    {
        $event = OrderCompletedEventBuilder::create()
            ->withAmount(9.99)
            ->build();  // Rest sind Defaults

        // Test small order handling...
    }
}

Praxis-Vorteil: Tests bleiben wartbar wenn Event-Konstruktoren sich ändern – nur der Builder muss angepasst werden!

6. Wie teste ich Event-Listener-Chains und Prioritäten?

Reihenfolge ist wichtig – testet sie explizit.

Priority-Test für komplexe Chains:

<?php

declare(strict_types=1);

namespace AppTestsEventListener;

use AppEventOrderCompletedEvent;
use PHPUnitFrameworkTestCase;
use SymfonyComponentEventDispatcherEventDispatcher;

class OrderListenerPriorityTest extends TestCase
{
    public function testListenersExecuteInCorrectOrder(): void
    {
        $dispatcher = new EventDispatcher();
        $executionOrder = [];

        // Priority 100: Validierung (muss zuerst laufen)
        $dispatcher->addListener(
            OrderCompletedEvent::class,
            function () use (&$executionOrder) {
                $executionOrder[] = 'validation';
            },
            100
        );

        // Priority 50: Business Logic
        $dispatcher->addListener(
            OrderCompletedEvent::class,
            function () use (&$executionOrder) {
                $executionOrder[] = 'business';
            },
            50
        );

        // Priority 0: Notification
        $dispatcher->addListener(
            OrderCompletedEvent::class,
            function () use (&$executionOrder) {
                $executionOrder[] = 'notification';
            },
            0
        );

        // Priority -50: Logging (sollte am Ende laufen)
        $dispatcher->addListener(
            OrderCompletedEvent::class,
            function () use (&$executionOrder) {
                $executionOrder[] = 'logging';
            },
            -50
        );

        $event = new OrderCompletedEvent(123, 99.99);
        $dispatcher->dispatch($event);

        $this->assertEquals(
            ['validation', 'business', 'notification', 'logging'],
            $executionOrder,
            'Listeners must execute in priority order'
        );
    }

    public function testStopPropagationPreventsLaterListeners(): void
    {
        $dispatcher = new EventDispatcher();
        $executed = [];

        $dispatcher->addListener(
            OrderCompletedEvent::class,
            function (OrderCompletedEvent $event) use (&$executed) {
                $executed[] = 'first';
                $event->stopPropagation();
            },
            10
        );

        $dispatcher->addListener(
            OrderCompletedEvent::class,
            function () use (&$executed) {
                $executed[] = 'second';  // Sollte nicht ausgeführt werden
            },
            5
        );

        $event = new OrderCompletedEvent(123, 99.99);
        $dispatcher->dispatch($event);

        $this->assertEquals(['first'], $executed);
        $this->assertTrue($event->isPropagationStopped());
    }
}

Consulting-Tipp: In Production haben wir erlebt, dass falsche Prioritäten zu subtilen Bugs führen – testet die Reihenfolge explizit!

7. Wie schreibe ich Integration-Tests für Events?

Integration-Tests prüfen das Zusammenspiel mehrerer Komponenten.

Symfony KernelTestCase für echte Integration:

<?php

declare(strict_types=1);

namespace AppTestsIntegration;

use AppEventOrderCompletedEvent;
use SymfonyBundleFrameworkBundleTestKernelTestCase;
use SymfonyComponentEventDispatcherEventDispatcherInterface;

class OrderCompletedIntegrationTest extends KernelTestCase
{
    private EventDispatcherInterface $dispatcher;

    protected function setUp(): void
    {
        self::bootKernel();

        $container = static::getContainer();
        $this->dispatcher = $container->get(EventDispatcherInterface::class);
    }

    public function testRealEventFlowWithAllListeners(): void
    {
        // Event mit echtem Dispatcher dispatchen
        $event = new OrderCompletedEvent(
            orderId: 12345,
            amount: 99.99,
            customerEmail: 'integration-test@example.com'
        );

        $this->dispatcher->dispatch($event);

        // Prüfe Seiteneffekte in echter Umgebung
        // z.B. Datenbank-Änderungen, gesendete E-Mails etc.

        // Beispiel: Prüfe ob Order-Status aktualisiert wurde
        $orderRepository = static::getContainer()
            ->get('doctrine')
            ->getRepository(Order::class);

        $order = $orderRepository->find(12345);
        $this->assertEquals('completed', $order->getStatus());
    }

    public function testAllSubscribersAreRegistered(): void
    {
        // Prüfe dass alle erwarteten Subscriber registriert sind
        $listeners = $this->dispatcher->getListeners(OrderCompletedEvent::class);

        $this->assertGreaterThan(
            0,
            count($listeners),
            'At least one listener should be registered'
        );

        // Optional: Prüfe spezifische Listener
        $listenerClasses = array_map(
            fn($listener) => get_class($listener[0] ?? $listener),
            $listeners
        );

        $this->assertContains(
            'AppEventListenerInvoiceGeneratorListener',
            $listenerClasses
        );
    }
}

Best Practice: Nutzt DAMADoctrineTestBundle für saubere Datenbank-Tests!

8. Wie teste ich asynchrone Event-Verarbeitung?

Messenger-Integration erfordert spezielle Test-Strategien.

Test für Event-to-Message Mapping:

<?php

declare(strict_types=1);

namespace AppTestsEventListener;

use AppEventOrderCompletedEvent;
use AppEventListenerAsyncOrderProcessingListener;
use AppMessageProcessOrderMessage;
use PHPUnitFrameworkTestCase;
use SymfonyComponentMessengerEnvelope;
use SymfonyComponentMessengerMessageBusInterface;

class AsyncOrderProcessingListenerTest extends TestCase
{
    public function testDispatchesAsyncMessage(): void
    {
        $messageBus = $this->createMock(MessageBusInterface::class);

        $messageBus
            ->expects($this->once())
            ->method('dispatch')
            ->with($this->callback(function ($message) {
                return $message instanceof ProcessOrderMessage
                    && $message->getOrderId() === 123;
            }))
            ->willReturn(new Envelope(new stdClass()));

        $listener = new AsyncOrderProcessingListener($messageBus);

        $event = new OrderCompletedEvent(123, 99.99);
        $listener->__invoke($event);
    }
}

Integration-Test mit Test-Transport:

// config/packages/test/messenger.yaml
framework:
    messenger:
        transports:
            async: 'in-memory://'

// Test
class MessengerIntegrationTest extends KernelTestCase
{
    public function testEventTriggersAsyncProcessing(): void
    {
        self::bootKernel();

        $container = static::getContainer();
        $dispatcher = $container->get(EventDispatcherInterface::class);
        $transport = $container->get('messenger.transport.async');

        $event = new OrderCompletedEvent(123, 99.99);
        $dispatcher->dispatch($event);

        // Prüfe Messages im Test-Transport
        $envelopes = $transport->get();
        $this->assertCount(1, $envelopes);

        $message = $envelopes[0]->getMessage();
        $this->assertInstanceOf(ProcessOrderMessage::class, $message);
    }
}

9. Wie erreiche ich hohe Code-Coverage für Event-Handler?

Coverage allein ist nicht das Ziel – aber diese Patterns helfen.

Edge-Cases systematisch testen:

class OrderValidationListenerTest extends TestCase
{
    /**
     * @dataProvider orderDataProvider
     */
    public function testValidationScenarios(
        OrderCompletedEvent $event,
        bool $shouldBeValid,
        ?string $expectedError
    ): void {
        $listener = new OrderValidationListener();

        try {
            $listener->__invoke($event);
            $this->assertTrue($shouldBeValid, 'Expected validation to fail');
        } catch (ValidationException $e) {
            $this->assertFalse($shouldBeValid, 'Expected validation to pass');
            $this->assertEquals($expectedError, $e->getMessage());
        }
    }

    public function orderDataProvider(): array
    {
        return [
            'valid order' => [
                new OrderCompletedEvent(1, 100.00, 'test@example.com'),
                true,
                null
            ],
            'negative amount' => [
                new OrderCompletedEvent(2, -10.00, 'test@example.com'),
                false,
                'Amount must be positive'
            ],
            'invalid email' => [
                new OrderCompletedEvent(3, 100.00, 'not-an-email'),
                false,
                'Invalid email address'
            ],
            'zero amount' => [
                new OrderCompletedEvent(4, 0.00, 'test@example.com'),
                false,
                'Amount must be greater than zero'
            ],
        ];
    }
}

Branch-Coverage für bedingte Logik:

public function testConditionalListenerExecution(): void
{
    $listener = new ConditionalOrderListener();

    // Test: Condition true
    $event = OrderCompletedEventBuilder::create()
        ->withAmount(1000.00)  // Über Threshold
        ->build();
    $listener->__invoke($event);
    $this->assertTrue($event->wasProcessedAsHighValue());

    // Test: Condition false
    $event = OrderCompletedEventBuilder::create()
        ->withAmount(50.00)  // Unter Threshold
        ->build();
    $listener->__invoke($event);
    $this->assertFalse($event->wasProcessedAsHighValue());
}

10. Wie debugge ich fehlschlagende Event-Tests?

Debugging-Strategien für komplexe Event-Szenarien.

Custom Test-Listener für Debugging:

class DebugEventListener
{
    private array $log = [];

    public function __invoke(object $event): void
    {
        $this->log[] = [
            'timestamp' => microtime(true),
            'event_class' => get_class($event),
            'event_data' => $this->serializeEvent($event),
            'backtrace' => debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3)
        ];
    }

    public function getLog(): array
    {
        return $this->log;
    }

    public function dumpLog(): void
    {
        foreach ($this->log as $entry) {
            echo sprintf(
                "[%s] %sn%snn",
                date('H:i:s', (int)$entry['timestamp']),
                $entry['event_class'],
                json_encode($entry['event_data'], JSON_PRETTY_PRINT)
            );
        }
    }

    private function serializeEvent(object $event): array
    {
        // Event-Properties extrahieren für Debugging
        $reflection = new ReflectionClass($event);
        $data = [];

        foreach ($reflection->getMethods() as $method) {
            if (str_starts_with($method->getName(), 'get')) {
                $property = lcfirst(substr($method->getName(), 3));
                try {
                    $data[$property] = $method->invoke($event);
                } catch (Throwable $e) {
                    $data[$property] = 'Error: ' . $e->getMessage();
                }
            }
        }

        return $data;
    }
}

Verwendung im Test:

public function testComplexEventFlow(): void
{
    $dispatcher = new EventDispatcher();
    $debugListener = new DebugEventListener();

    // Debug-Listener mit höchster Priority
    $dispatcher->addListener(
        OrderCompletedEvent::class,
        $debugListener,
        PHP_INT_MAX
    );

    // Deine echten Listener
    // ...

    try {
        $dispatcher->dispatch(new OrderCompletedEvent(123, 99.99));
    } catch (Throwable $e) {
        // Bei Fehler: Event-Flow ausgeben
        $debugListener->dumpLog();
        throw $e;
    }
}

PHPUnit Listener für globales Event-Tracking:

// phpunit.xml.dist
<extensions>
    <bootstrap class="AppTestsEventTrackingExtension"/>
</extensions>

// EventTrackingExtension.php
class EventTrackingExtension implements Extension
{
    public function executeBeforeTest(string $test): void
    {
        // Event-Tracking aktivieren
        EventTracker::reset();
    }

    public function executeAfterTest(string $test, float $time): void
    {
        // Bei Failure: Events loggen
        if (EventTracker::hasEvents()) {
            echo "nnDispatched Events:n";
            echo EventTracker::getSummary();
        }
    }
}

Best Practices aus über 15 Jahren Testing-Erfahrung

Nach unzähligen Projekten haben wir bei Never Code Alone folgende Standards etabliert:

Real over Mock: Nutzt echte Dispatcher und Event-Objekte, mockt nur externe Dependencies

Test Isolation: Jeder Test sollte unabhängig laufen können ohne Seiteneffekte

Arrange-Act-Assert: Klare Struktur in jedem Test für bessere Lesbarkeit

Data Providers: Nutzt DataProviders für Edge-Cases statt Copy-Paste-Tests

Builder Pattern: Event-Builder für wartbare Test-Fixtures

Integration Tests: Mindestens ein Integration-Test pro Event-Flow

Explicit Priorities: Testet Listener-Reihenfolge wenn sie wichtig ist

Der entscheidende Vorteil für eure Projekte

Richtig getestete Events sind mehr als Qualitätssicherung – sie sind Living Documentation eurer Architektur. Gut getestete Event-Systeme:

  • Reduzieren Production-Bugs um bis zu 80% in unseren Projekten
  • Beschleunigen Refactorings durch Vertrauen in Tests
  • Dokumentieren Event-Flows besser als jedes Diagramm
  • Ermöglichen sicheres Deployment durch CI/CD-Integration
  • Onboarden neue Developer durch lesbare Test-Beispiele

Direkte Unterstützung für euer Team

Ihr wollt eure Test-Coverage für Events verbessern? Oder braucht ihr Unterstützung bei der Migration von Mocks zu echten Tests? Mit über 15 Jahren Expertise in Softwarequalität, Open Source und Remote Consulting helfen wir euch gerne weiter.

Kontakt: roland@nevercodealone.de

Gemeinsam schaffen wir robuste Test-Suites, die euer Team voranbringen – keine theoretischen Konzepte, sondern praktische Lösungen die funktionieren.

Fazit: Tests die Vertrauen schaffen

Event-Testing mag auf den ersten Blick komplex erscheinen, aber mit den richtigen Patterns wird es zum natürlichen Teil eures Workflows. Von der ersten Event-Definition bis zum Production-Deployment – gute Tests sind euer Sicherheitsnetz für mutige Refactorings und schnelle Feature-Entwicklung.

Startet heute: Nehmt einen gemockten Event-Test in eurem Projekt und refactored ihn zu einem Test mit echtem Dispatcher. Der Qualitätsgewinn ist der erste Schritt zu besserer Software-Qualität.

Never Code Alone – Gemeinsam für bessere Software-Qualität!

0 Kommentar

Tutorials und Top Posts

Gib uns Feedback

Diese Seite benutzt Cookies. Ein Akzeptieren hilft uns die Seite zu verbessern. Ok Mehr dazu