Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions docs/2-features/08-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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.
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions packages/event-bus/src/EventBus.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
2 changes: 1 addition & 1 deletion packages/event-bus/src/GenericEventBus.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 26 additions & 10 deletions packages/event-bus/src/Testing/EventBusTester.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
*
Expand All @@ -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),
Expand All @@ -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.');

Expand All @@ -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),
Expand Down Expand Up @@ -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;
}
Expand Down
24 changes: 18 additions & 6 deletions packages/event-bus/src/Testing/FakeEventBus.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string|object> */
public array $dispatched = [];

/** @var array<string,array<CallableEventHandler>> */
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);
}
}
Loading