diff --git a/docs/2-features/08-events.md b/docs/2-features/08-events.md index bcc154bda..c88964de2 100644 --- a/docs/2-features/08-events.md +++ b/docs/2-features/08-events.md @@ -194,11 +194,14 @@ Other events include migration-related ones, such as {b`Tempest\Database\Migrati ## Testing -By extending {`Tempest\Framework\Testing\IntegrationTest`} from your test case, you may gain access to the event bus testing utilities using the `eventBus` property. +By extending {b`Tempest\Framework\Testing\IntegrationTest`} from your test case, you gain access to the event bus testing utilities through the `eventBus` property. 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 +// Record dispatched events for assertion +$this->eventBus->recordEventDispatches(); + // Prevents events from being handled $this->eventBus->preventEventHandling(); @@ -222,16 +225,22 @@ $this->eventBus->assertNotDispatched(AircraftRegistered::class); $this->eventBus->assertListeningTo(AircraftRegistered::class); ``` -### Preventing event handling +### Recording event dispatches 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. -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, the event bus instance must be replaced with a testing implementation in the container. This is achieved by calling the `preventEventHandling()` method on the `eventBus` property. -```php tests/MyServiceTest.php +```php $this->eventBus->preventEventHandling(); ``` +If you want to be able to make assertions while still allowing events to be dispatched, you may instead call the `recordEventDispatches()` method. + +```php +$this->eventBus->recordEventDispatches(); +``` + ### Testing a method-based handler When handlers are registered as methods, instead of dispatching the corresponding event to test the handler logic, you may simply call the method to test it in isolation. @@ -252,7 +261,7 @@ final readonly class AircraftObserver This handler may be tested by resolving the service class from the container, and calling the method with an instance of the event created for this purpose. ```php app/AircraftObserverTest.php -// Replace the event bus in the container +// Prevent events from being handled while allowing assertions $this->eventBus->preventEventHandling(); // Resolve the service class diff --git a/packages/event-bus/src/EventBus.php b/packages/event-bus/src/EventBus.php index 445e9a0e3..e95502387 100644 --- a/packages/event-bus/src/EventBus.php +++ b/packages/event-bus/src/EventBus.php @@ -8,7 +8,13 @@ interface EventBus { + /** + * Dispatches the given event to all its listeners. The event can be a string, a FQCN or an plain old PHP object. + */ public function dispatch(string|object $event): void; + /** + * Adds a listener for the given event. The closure accepts the event object as its first parameter, so the `$event` parameter is optional. + */ public function listen(Closure $handler, ?string $event = null): void; } diff --git a/packages/event-bus/src/GenericEventBus.php b/packages/event-bus/src/GenericEventBus.php index 1e2284114..5212f0bc1 100644 --- a/packages/event-bus/src/GenericEventBus.php +++ b/packages/event-bus/src/GenericEventBus.php @@ -15,7 +15,7 @@ { public function __construct( private Container $container, - private EventBusConfig $eventBusConfig, + private(set) EventBusConfig $eventBusConfig, ) {} public function listen(Closure $handler, ?string $event = null): void diff --git a/packages/event-bus/src/Testing/EventBusTester.php b/packages/event-bus/src/Testing/EventBusTester.php index f3d3a8fa7..b1409ecf4 100644 --- a/packages/event-bus/src/Testing/EventBusTester.php +++ b/packages/event-bus/src/Testing/EventBusTester.php @@ -6,7 +6,6 @@ use PHPUnit\Framework\Assert; use Tempest\Container\Container; use Tempest\EventBus\EventBus; -use Tempest\EventBus\EventBusConfig; use Tempest\Support\Str; final class EventBusTester @@ -18,16 +17,30 @@ public function __construct( ) {} /** - * Prevents the registered event handlers from being called. + * Records event dispatches, and optionally prevents the registered event handlers from being called. + * + * @param bool $preventHandling Whether to prevent the registered event handlers from being called while still allowing assertions. */ - public function preventEventHandling(): self + public function recordEventDispatches(bool $preventHandling = false): self { - $this->fakeEventBus = new FakeEventBus($this->container->get(EventBusConfig::class)); + $this->fakeEventBus = new FakeEventBus( + genericEventBus: $this->container->get(EventBus::class), + preventHandling: $preventHandling, + ); + $this->container->singleton(EventBus::class, $this->fakeEventBus); return $this; } + /** + * Prevents the registered event handlers from being called. + */ + public function preventEventHandling(): self + { + return $this->recordEventDispatches(preventHandling: true); + } + /** * Asserts that the given `$event` has been dispatched. * @@ -36,7 +49,7 @@ public function preventEventHandling(): self */ public function assertDispatched(string|object $event, ?Closure $callback = null, ?int $count = null): self { - $this->assertFaked(); + $this->assertRecording(); Assert::assertNotEmpty( actual: $dispatches = $this->findDispatches($event), @@ -61,7 +74,7 @@ public function assertDispatched(string|object $event, ?Closure $callback = null */ public function assertNotDispatched(string|object $event): self { - $this->assertFaked(); + $this->assertRecording(); Assert::assertEmpty($this->findDispatches($event), 'The event was dispatched.'); @@ -75,7 +88,7 @@ public function assertNotDispatched(string|object $event): self */ public function assertListeningTo(string $event, ?int $count = null): self { - $this->assertFaked(); + $this->assertRecording(); Assert::assertNotEmpty( actual: $handlers = $this->findHandlersFor($event), @@ -109,12 +122,15 @@ private function findHandlersFor(string|object $event): array { $eventName = Str\parse($event) ?: $event::class; - return $this->fakeEventBus->eventBusConfig->handlers[$eventName] ?? []; + return $this->fakeEventBus->handlers[$eventName] ?? []; } - private function assertFaked(): self + private function assertRecording(): self { - Assert::assertTrue(isset($this->fakeEventBus), 'Asserting against the event bus require the `preventEventHandling()` method to be called first.'); + Assert::assertTrue( + isset($this->fakeEventBus), + 'Asserting against the event bus require the `recordEventHandling()` or `preventEventHandling()` method to be called first.', + ); return $this; } diff --git a/packages/event-bus/src/Testing/FakeEventBus.php b/packages/event-bus/src/Testing/FakeEventBus.php index 841b703d6..4e702f4ea 100644 --- a/packages/event-bus/src/Testing/FakeEventBus.php +++ b/packages/event-bus/src/Testing/FakeEventBus.php @@ -3,24 +3,36 @@ namespace Tempest\EventBus\Testing; use Closure; +use Tempest\EventBus\CallableEventHandler; use Tempest\EventBus\EventBus; -use Tempest\EventBus\EventBusConfig; +use Tempest\EventBus\GenericEventBus; final class FakeEventBus implements EventBus { + /** @var array */ public array $dispatched = []; + /** @var array> */ + public array $handlers { + get => $this->genericEventBus->eventBusConfig->handlers; + } + public function __construct( - public EventBusConfig $eventBusConfig, + private(set) GenericEventBus $genericEventBus, + public bool $preventHandling = true, ) {} - public function listen(Closure $handler, ?string $event = null): void + public function dispatch(string|object $event): void { - $this->eventBusConfig->addClosureHandler($handler, $event); + $this->dispatched[] = $event; + + if ($this->preventHandling === false) { + $this->genericEventBus->dispatch($event); + } } - public function dispatch(string|object $event): void + public function listen(Closure $handler, ?string $event = null): void { - $this->dispatched[] = $event; + $this->genericEventBus->listen($handler, $event); } } diff --git a/tests/Integration/EventBus/EventBusTesterTest.php b/tests/Integration/EventBus/EventBusTesterTest.php index f8ee38709..1c4c41900 100644 --- a/tests/Integration/EventBus/EventBusTesterTest.php +++ b/tests/Integration/EventBus/EventBusTesterTest.php @@ -5,6 +5,7 @@ namespace Tests\Tempest\Integration\EventBus; use LogicException; +use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\ExpectationFailedException; use Tempest\EventBus\EventBus; use Tempest\EventBus\Testing\FakeEventBus; @@ -16,109 +17,122 @@ */ final class EventBusTesterTest extends FrameworkIntegrationTestCase { - public function test_fake(): void + private EventBus $bus { + get => $this->container->get(EventBus::class); + } + + #[Test] + public function fake(): void { $this->eventBus->preventEventHandling(); - $this->assertInstanceOf(FakeEventBus::class, $this->container->get(EventBus::class)); + $this->assertInstanceOf(FakeEventBus::class, $this->bus); } - public function test_assertion_on_real_event_bus(): void + #[Test] + public function assertion_on_real_event_bus(): void { $this->expectException(ExpectationFailedException::class); - $this->expectExceptionMessage('Asserting against the event bus require the `preventEventHandling()` method to be called first.'); + $this->expectExceptionMessageMatches('*Asserting against the event bus require*'); $this->eventBus->assertDispatched('event-bus-fake-event'); } - public function test_assert_dispatched(): void + #[Test] + public function assert_dispatched(): void { $this->eventBus->preventEventHandling(); - $this->container->get(EventBus::class)->dispatch('event-bus-fake-event'); + $this->bus->dispatch('event-bus-fake-event'); $this->eventBus->assertDispatched('event-bus-fake-event'); - $this->container->get(EventBus::class)->dispatch(new FakeEvent('foo')); + $this->bus->dispatch(new FakeEvent('foo')); $this->eventBus->assertDispatched(FakeEvent::class); } - public function test_assert_dispatched_with_callback(): void + #[Test] + public function assert_dispatched_with_callback(): void { $this->eventBus->preventEventHandling(); - $this->container->get(EventBus::class)->dispatch('event-bus-fake-event'); + $this->bus->dispatch('event-bus-fake-event'); $this->eventBus->assertDispatched('event-bus-fake-event', function (string $event) { return $event === 'event-bus-fake-event'; }); - $this->container->get(EventBus::class)->dispatch(new FakeEvent('foo')); + $this->bus->dispatch(new FakeEvent('foo')); $this->eventBus->assertDispatched(FakeEvent::class, function (FakeEvent $event) { return $event->value === 'foo'; }); } - public function test_assert_dispatched_with_count(): void + #[Test] + public function assert_dispatched_with_count(): void { $this->eventBus->preventEventHandling(); - $this->container->get(EventBus::class)->dispatch('event-bus-fake-event'); + $this->bus->dispatch('event-bus-fake-event'); $this->eventBus->assertDispatched('event-bus-fake-event', count: 1); - $this->container->get(EventBus::class)->dispatch('event-bus-fake-event'); + $this->bus->dispatch('event-bus-fake-event'); $this->eventBus->assertDispatched('event-bus-fake-event', count: 2); - $this->container->get(EventBus::class)->dispatch(new FakeEvent('foo')); + $this->bus->dispatch(new FakeEvent('foo')); $this->eventBus->assertDispatched(FakeEvent::class, count: 1); - $this->container->get(EventBus::class)->dispatch(new FakeEvent('foo')); + $this->bus->dispatch(new FakeEvent('foo')); $this->eventBus->assertDispatched(FakeEvent::class, count: 2); - $this->container->get(EventBus::class)->dispatch(new FakeEvent('baz')); + $this->bus->dispatch(new FakeEvent('baz')); $this->eventBus->assertDispatched(FakeEvent::class, count: 3); } - public function test_assert_dispatched_with_count_failure(): void + #[Test] + public function assert_dispatched_with_count_failure(): void { $this->expectException(ExpectationFailedException::class); $this->expectExceptionMessage('The number of dispatches does not match'); $this->eventBus->preventEventHandling(); - $this->container->get(EventBus::class)->dispatch('event-bus-fake-event'); + $this->bus->dispatch('event-bus-fake-event'); $this->eventBus->assertDispatched('event-bus-fake-event', count: 2); } - public function test_assert_dispatched_with_callback_failure(): void + #[Test] + public function assert_dispatched_with_callback_failure(): void { $this->expectException(ExpectationFailedException::class); $this->expectExceptionMessage('The callback failed'); $this->eventBus->preventEventHandling(); - $this->container->get(EventBus::class)->dispatch('event-bus-fake-event'); + $this->bus->dispatch('event-bus-fake-event'); $this->eventBus->assertDispatched('event-bus-fake-event', function (string $event) { return $event !== 'event-bus-fake-event'; }); } - public function test_assert_dispatched_object_with_callback_failure(): void + #[Test] + public function assert_dispatched_object_with_callback_failure(): void { $this->expectException(ExpectationFailedException::class); $this->expectExceptionMessage('The callback failed'); $this->eventBus->preventEventHandling(); - $this->container->get(EventBus::class)->dispatch(new FakeEvent('foo')); + $this->bus->dispatch(new FakeEvent('foo')); $this->eventBus->assertDispatched(FakeEvent::class, function (FakeEvent $event) { return $event->value === 'foobar'; }); } - public function test_assert_dispatched_failure(): void + #[Test] + public function assert_dispatched_failure(): void { $this->eventBus->preventEventHandling(); - $this->container->get(EventBus::class)->dispatch('event-bus-fake-event'); + $this->bus->dispatch('event-bus-fake-event'); $this->expectException(ExpectationFailedException::class); $this->expectExceptionMessage('The event was not dispatched.'); @@ -126,72 +140,72 @@ public function test_assert_dispatched_failure(): void $this->eventBus->assertDispatched('this-was-not-dispatched'); } - public function test_assert_not_dispatched(): void + #[Test] + public function assert_not_dispatched(): void { $this->eventBus->preventEventHandling(); - $this->container->get(EventBus::class)->dispatch('event-bus-fake-event'); + $this->bus->dispatch('event-bus-fake-event'); $this->eventBus->assertNotDispatched('this-was-not-dispatched'); } - public function test_assert_not_dispatched_failure(): void + #[Test] + public function assert_not_dispatched_failure(): void { $this->expectException(ExpectationFailedException::class); $this->expectExceptionMessage('The event was dispatched'); $this->eventBus->preventEventHandling(); - $this->container->get(EventBus::class)->dispatch('event-bus-fake-event'); + $this->bus->dispatch('event-bus-fake-event'); $this->eventBus->assertNotDispatched('event-bus-fake-event'); } - public function test_assert_not_dispatched_object_failure(): void + #[Test] + public function assert_not_dispatched_object_failure(): void { $this->expectException(ExpectationFailedException::class); $this->expectExceptionMessage('The event was dispatched'); $this->eventBus->preventEventHandling(); - $this->container->get(EventBus::class)->dispatch(new FakeEvent('foo')); + $this->bus->dispatch(new FakeEvent('foo')); $this->eventBus->assertNotDispatched(FakeEvent::class); } - public function test_assert_listening_to(): void + #[Test] + public function assert_listening_to(): void { $this->eventBus->preventEventHandling(); - $this->container - ->get(EventBus::class) - ->listen(function (FakeEvent $_): never { - throw new LogicException('This should not be called'); - }); + $this->bus->listen(function (FakeEvent $_): never { + throw new LogicException('This should not be called'); + }); $this->eventBus->assertListeningTo(FakeEvent::class); $this->eventBus->assertListeningTo(FakeEvent::class); } - public function test_assert_listening_to_count(): void + #[Test] + public function assert_listening_to_count(): void { $this->eventBus->preventEventHandling(); - $this->container - ->get(EventBus::class) - ->listen(function (FakeEvent $_): never { - throw new LogicException('This should not be called'); - }); + $this->bus->listen(function (FakeEvent $_): never { + throw new LogicException('This should not be called'); + }); $this->eventBus->assertListeningTo(FakeEvent::class, count: 1); - $this->container - ->get(EventBus::class) - ->listen(function (FakeEvent $_): never { - throw new LogicException('This should not be called'); - }); + $this->bus->listen(function (FakeEvent $_): never { + throw new LogicException('This should not be called'); + }); $this->eventBus->assertListeningTo(FakeEvent::class, count: 2); } - public function test_assert_listening_to_failure(): void + #[Test] + public function assert_listening_to_failure(): void { $this->expectException(ExpectationFailedException::class); $this->expectExceptionMessage('The event is not being listened to'); @@ -201,19 +215,35 @@ public function test_assert_listening_to_failure(): void $this->eventBus->assertListeningTo(FakeEvent::class); } - public function test_assert_listening_to_count_failure(): void + #[Test] + public function assert_listening_to_count_failure(): void { $this->expectException(ExpectationFailedException::class); $this->expectExceptionMessage('The number of handlers does not match'); $this->eventBus->preventEventHandling(); - $this->container - ->get(EventBus::class) - ->listen(function (FakeEvent $_): never { - throw new LogicException('This should not be called'); - }); + $this->bus->listen(function (FakeEvent $_): never { + throw new LogicException('This should not be called'); + }); $this->eventBus->assertListeningTo(FakeEvent::class, count: 2); } + + public function allows_handling(): void + { + $this->eventBus->recordEventDispatches(); + + $handled = false; + $this->bus->listen(function (FakeEvent $_) use (&$handled): void { + $handled = true; + }); + + $this->bus->dispatch(new FakeEvent('foo')); + + $this->eventBus->assertDispatched(FakeEvent::class); + $this->eventBus->assertListeningTo(FakeEvent::class); + + $this->assertTrue($handled); + } }