diff --git a/docs/2-features/08-events.md b/docs/2-features/08-events.md index e259656d4..59947f55a 100644 --- a/docs/2-features/08-events.md +++ b/docs/2-features/08-events.md @@ -174,9 +174,6 @@ By extending {`Tempest\Framework\Testing\IntegrationTest`} from your test case, These utilities include a way to replace the event bus with a testing implementation, as well as a few assertion methods to ensure that events have been dispatched or are being listened to. ```php -// Prevents events from being handled -$this->eventBus->preventEventHandling(); - // Assert that an event has been dispatched $this->eventBus->assertDispatched(AircraftRegistered::class); @@ -197,14 +194,24 @@ $this->eventBus->assertNotDispatched(AircraftRegistered::class); $this->eventBus->assertListeningTo(AircraftRegistered::class); ``` -### Preventing event handling +**Note:** Event assertions now work automatically without calling `preventEventHandling()`. Events are tracked while still executing their handlers, allowing you to test both event dispatching and their side effects together. + +### Preventing event handling (optional) -When testing code that dispatches events, you may want to prevent Tempest from handling them. This can be useful when the event’s handlers are tested separately, or when the side-effects of these handlers are not desired for this test case. +While event assertions now work without preventing handler execution, you may still want to prevent event handlers from running in certain test scenarios. This can be useful when: +- The event handlers are tested separately +- The side-effects of handlers are not desired for the test +- You want to isolate the event dispatching logic from handler execution -To disable event handling, the event bus instance must be replaced with a testing implementation in the container. This may be achieved by calling the `preventEventHandling()` method on the `eventBus` property. +To disable event handling, call the `preventEventHandling()` method on the `eventBus` property: ```php tests/MyServiceTest.php +// Optionally prevent handlers from executing $this->eventBus->preventEventHandling(); + +// Events will still be tracked for assertions, +// but their handlers won't be executed +$this->eventBus->assertDispatched(AircraftRegistered::class); ``` ### Testing a method-based handler diff --git a/packages/event-bus/src/Testing/EventBusTester.php b/packages/event-bus/src/Testing/EventBusTester.php index c8af4b9a0..8c16e9197 100644 --- a/packages/event-bus/src/Testing/EventBusTester.php +++ b/packages/event-bus/src/Testing/EventBusTester.php @@ -5,8 +5,6 @@ use BackedEnum; use Closure; use PHPUnit\Framework\Assert; -use PHPUnit\Framework\ExpectationFailedException; -use PHPUnit\Framework\GeneratorNotSupportedException; use Tempest\Container\Container; use Tempest\EventBus\EventBus; use Tempest\EventBus\EventBusConfig; @@ -14,11 +12,14 @@ final class EventBusTester { - private FakeEventBus $fakeEventBus; + private ?FakeEventBus $fakeEventBus = null; + private ?TrackedEventBus $trackedEventBus = null; public function __construct( private readonly Container $container, - ) {} + ) { + $this->installTrackedBus(); + } /** * Prevents the registered event handlers from being called. @@ -31,6 +32,43 @@ public function preventEventHandling(): self return $this; } + private function installTrackedBus(): void + { + $current = $this->container->get(EventBus::class); + + if ($current instanceof FakeEventBus) { + $this->fakeEventBus = $current; + + return; + } + + if ($current instanceof TrackedEventBus) { + $this->trackedEventBus = $current; + + return; + } + + $this->trackedEventBus = new TrackedEventBus( + inner: $current, + eventBusConfig: $this->container->get(EventBusConfig::class), + ); + + $this->container->singleton(EventBus::class, $this->trackedEventBus); + } + + private function getInspectionBus(): FakeEventBus|TrackedEventBus + { + if ($this->fakeEventBus !== null) { + return $this->fakeEventBus; + } + + if ($this->trackedEventBus === null) { + $this->installTrackedBus(); + } + + return $this->trackedEventBus; + } + /** * Asserts that the given `$event` has been dispatched. * @@ -39,12 +77,9 @@ public function preventEventHandling(): self */ public function assertDispatched(string|object $event, ?Closure $callback = null, ?int $count = null): self { - $this->assertFaked(); + $dispatches = $this->findDispatches($event); - Assert::assertNotNull( - actual: $dispatches = $this->findDispatches($event), - message: 'The event was not dispatched.', - ); + Assert::assertNotEmpty($dispatches, 'The event was not dispatched.'); if ($count !== null) { Assert::assertCount($count, $dispatches, 'The number of dispatches does not match.'); @@ -64,8 +99,6 @@ public function assertDispatched(string|object $event, ?Closure $callback = null */ public function assertNotDispatched(string|object $event): self { - $this->assertFaked(); - Assert::assertEmpty($this->findDispatches($event), 'The event was dispatched.'); return $this; @@ -78,8 +111,6 @@ public function assertNotDispatched(string|object $event): self */ public function assertListeningTo(string $event, ?int $count = null): self { - $this->assertFaked(); - Assert::assertNotEmpty( actual: $handlers = $this->findHandlersFor($event), message: 'The event is not being listened to.', @@ -94,7 +125,19 @@ public function assertListeningTo(string $event, ?int $count = null): self private function findDispatches(string|object $event): array { - return array_filter($this->fakeEventBus->dispatched, function (string|object $dispatched) use ($event) { + // Collect dispatched events from both buses + // This handles the case where components hold references to the old EventBus and continue dispatching to it + $allDispatched = []; + + if ($this->trackedEventBus !== null) { + $allDispatched = array_merge($allDispatched, $this->trackedEventBus->dispatched); + } + + if ($this->fakeEventBus !== null) { + $allDispatched = array_merge($allDispatched, $this->fakeEventBus->dispatched); + } + + return array_filter($allDispatched, function (string|object $dispatched) use ($event) { if ($dispatched === $event) { return true; } @@ -117,13 +160,8 @@ private function findHandlersFor(string|object $event): array default => $event::class, }; - return $this->fakeEventBus->eventBusConfig->handlers[$eventName] ?? []; - } - - private function assertFaked(): self - { - Assert::assertTrue(isset($this->fakeEventBus), 'Asserting against the event bus require the `preventEventHandling()` method to be called first.'); + $inspectionBus = $this->getInspectionBus(); - return $this; + return $inspectionBus->eventBusConfig->handlers[$eventName] ?? []; } } diff --git a/packages/event-bus/src/Testing/TrackedEventBus.php b/packages/event-bus/src/Testing/TrackedEventBus.php new file mode 100644 index 000000000..39fc0760c --- /dev/null +++ b/packages/event-bus/src/Testing/TrackedEventBus.php @@ -0,0 +1,31 @@ +inner->listen($handler, $event); + } + + public function dispatch(string|object $event): void + { + $this->dispatched[] = $event; + + $this->inner->dispatch($event); + } +} diff --git a/tests/Integration/EventBus/EventBusTesterTest.php b/tests/Integration/EventBus/EventBusTesterTest.php index 85f262437..b50dc0b92 100644 --- a/tests/Integration/EventBus/EventBusTesterTest.php +++ b/tests/Integration/EventBus/EventBusTesterTest.php @@ -24,9 +24,32 @@ public function test_fake(): void } public function test_assertion_on_real_event_bus(): void + { + $this->container->get(EventBus::class)->dispatch('event-bus-fake-event'); + + $this->eventBus->assertDispatched('event-bus-fake-event'); + } + + public function test_tracked_event_bus_executes_handlers(): void + { + $called = false; + + $this->container + ->get(EventBus::class) + ->listen(function () use (&$called): void { + $called = true; + }, FakeEvent::class); + + $this->container->get(EventBus::class)->dispatch(new FakeEvent('foo')); + + $this->assertTrue($called); + $this->eventBus->assertDispatched(FakeEvent::class); + } + + public function test_assert_dispatched_without_count_and_no_event_failure(): void { $this->expectException(ExpectationFailedException::class); - $this->expectExceptionMessage('Asserting against the event bus require the `preventEventHandling()` method to be called first.'); + $this->expectExceptionMessage('The event was not dispatched'); $this->eventBus->assertDispatched('event-bus-fake-event'); }