Skip to content
Closed
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: 13 additions & 6 deletions docs/2-features/08-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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
Expand Down
80 changes: 59 additions & 21 deletions packages/event-bus/src/Testing/EventBusTester.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,21 @@
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;
use UnitEnum;

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.
Expand All @@ -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.
*
Expand All @@ -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.');
Expand All @@ -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;
Expand All @@ -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.',
Expand All @@ -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;
}
Expand All @@ -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] ?? [];
}
}
31 changes: 31 additions & 0 deletions packages/event-bus/src/Testing/TrackedEventBus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Tempest\EventBus\Testing;

use Closure;
use Tempest\EventBus\EventBus;
use Tempest\EventBus\EventBusConfig;

final class TrackedEventBus implements EventBus
{
public array $dispatched = [];

public function __construct(
public EventBus $inner,
public EventBusConfig $eventBusConfig,
) {}

public function listen(Closure $handler, ?string $event = null): void
{
$this->inner->listen($handler, $event);
}

public function dispatch(string|object $event): void
{
$this->dispatched[] = $event;

$this->inner->dispatch($event);
}
}
25 changes: 24 additions & 1 deletion tests/Integration/EventBus/EventBusTesterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
Expand Down
Loading