Skip to content

Commit ea48a8a

Browse files
authored
feat(event-bus): allow assertions without preventing event execution (#1841)
1 parent d75d54c commit ea48a8a

File tree

6 files changed

+149
-76
lines changed

6 files changed

+149
-76
lines changed

docs/2-features/08-events.md

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -194,11 +194,14 @@ Other events include migration-related ones, such as {b`Tempest\Database\Migrati
194194

195195
## Testing
196196

197-
By extending {`Tempest\Framework\Testing\IntegrationTest`} from your test case, you may gain access to the event bus testing utilities using the `eventBus` property.
197+
By extending {b`Tempest\Framework\Testing\IntegrationTest`} from your test case, you gain access to the event bus testing utilities through the `eventBus` property.
198198

199199
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.
200200

201201
```php
202+
// Record dispatched events for assertion
203+
$this->eventBus->recordEventDispatches();
204+
202205
// Prevents events from being handled
203206
$this->eventBus->preventEventHandling();
204207

@@ -222,16 +225,22 @@ $this->eventBus->assertNotDispatched(AircraftRegistered::class);
222225
$this->eventBus->assertListeningTo(AircraftRegistered::class);
223226
```
224227

225-
### Preventing event handling
228+
### Recording event dispatches
226229

227230
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.
228231

229-
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.
232+
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.
230233

231-
```php tests/MyServiceTest.php
234+
```php
232235
$this->eventBus->preventEventHandling();
233236
```
234237

238+
If you want to be able to make assertions while still allowing events to be dispatched, you may instead call the `recordEventDispatches()` method.
239+
240+
```php
241+
$this->eventBus->recordEventDispatches();
242+
```
243+
235244
### Testing a method-based handler
236245

237246
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
252261
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.
253262

254263
```php app/AircraftObserverTest.php
255-
// Replace the event bus in the container
264+
// Prevent events from being handled while allowing assertions
256265
$this->eventBus->preventEventHandling();
257266

258267
// Resolve the service class

packages/event-bus/src/EventBus.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@
88

99
interface EventBus
1010
{
11+
/**
12+
* Dispatches the given event to all its listeners. The event can be a string, a FQCN or an plain old PHP object.
13+
*/
1114
public function dispatch(string|object $event): void;
1215

16+
/**
17+
* Adds a listener for the given event. The closure accepts the event object as its first parameter, so the `$event` parameter is optional.
18+
*/
1319
public function listen(Closure $handler, ?string $event = null): void;
1420
}

packages/event-bus/src/GenericEventBus.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
{
1616
public function __construct(
1717
private Container $container,
18-
private EventBusConfig $eventBusConfig,
18+
private(set) EventBusConfig $eventBusConfig,
1919
) {}
2020

2121
public function listen(Closure $handler, ?string $event = null): void

packages/event-bus/src/Testing/EventBusTester.php

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
use PHPUnit\Framework\Assert;
77
use Tempest\Container\Container;
88
use Tempest\EventBus\EventBus;
9-
use Tempest\EventBus\EventBusConfig;
109
use Tempest\Support\Str;
1110

1211
final class EventBusTester
@@ -18,16 +17,30 @@ public function __construct(
1817
) {}
1918

2019
/**
21-
* Prevents the registered event handlers from being called.
20+
* Records event dispatches, and optionally prevents the registered event handlers from being called.
21+
*
22+
* @param bool $preventHandling Whether to prevent the registered event handlers from being called while still allowing assertions.
2223
*/
23-
public function preventEventHandling(): self
24+
public function recordEventDispatches(bool $preventHandling = false): self
2425
{
25-
$this->fakeEventBus = new FakeEventBus($this->container->get(EventBusConfig::class));
26+
$this->fakeEventBus = new FakeEventBus(
27+
genericEventBus: $this->container->get(EventBus::class),
28+
preventHandling: $preventHandling,
29+
);
30+
2631
$this->container->singleton(EventBus::class, $this->fakeEventBus);
2732

2833
return $this;
2934
}
3035

36+
/**
37+
* Prevents the registered event handlers from being called.
38+
*/
39+
public function preventEventHandling(): self
40+
{
41+
return $this->recordEventDispatches(preventHandling: true);
42+
}
43+
3144
/**
3245
* Asserts that the given `$event` has been dispatched.
3346
*
@@ -36,7 +49,7 @@ public function preventEventHandling(): self
3649
*/
3750
public function assertDispatched(string|object $event, ?Closure $callback = null, ?int $count = null): self
3851
{
39-
$this->assertFaked();
52+
$this->assertRecording();
4053

4154
Assert::assertNotEmpty(
4255
actual: $dispatches = $this->findDispatches($event),
@@ -61,7 +74,7 @@ public function assertDispatched(string|object $event, ?Closure $callback = null
6174
*/
6275
public function assertNotDispatched(string|object $event): self
6376
{
64-
$this->assertFaked();
77+
$this->assertRecording();
6578

6679
Assert::assertEmpty($this->findDispatches($event), 'The event was dispatched.');
6780

@@ -75,7 +88,7 @@ public function assertNotDispatched(string|object $event): self
7588
*/
7689
public function assertListeningTo(string $event, ?int $count = null): self
7790
{
78-
$this->assertFaked();
91+
$this->assertRecording();
7992

8093
Assert::assertNotEmpty(
8194
actual: $handlers = $this->findHandlersFor($event),
@@ -109,12 +122,15 @@ private function findHandlersFor(string|object $event): array
109122
{
110123
$eventName = Str\parse($event) ?: $event::class;
111124

112-
return $this->fakeEventBus->eventBusConfig->handlers[$eventName] ?? [];
125+
return $this->fakeEventBus->handlers[$eventName] ?? [];
113126
}
114127

115-
private function assertFaked(): self
128+
private function assertRecording(): self
116129
{
117-
Assert::assertTrue(isset($this->fakeEventBus), 'Asserting against the event bus require the `preventEventHandling()` method to be called first.');
130+
Assert::assertTrue(
131+
isset($this->fakeEventBus),
132+
'Asserting against the event bus require the `recordEventHandling()` or `preventEventHandling()` method to be called first.',
133+
);
118134

119135
return $this;
120136
}

packages/event-bus/src/Testing/FakeEventBus.php

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,36 @@
33
namespace Tempest\EventBus\Testing;
44

55
use Closure;
6+
use Tempest\EventBus\CallableEventHandler;
67
use Tempest\EventBus\EventBus;
7-
use Tempest\EventBus\EventBusConfig;
8+
use Tempest\EventBus\GenericEventBus;
89

910
final class FakeEventBus implements EventBus
1011
{
12+
/** @var array<string|object> */
1113
public array $dispatched = [];
1214

15+
/** @var array<string,array<CallableEventHandler>> */
16+
public array $handlers {
17+
get => $this->genericEventBus->eventBusConfig->handlers;
18+
}
19+
1320
public function __construct(
14-
public EventBusConfig $eventBusConfig,
21+
private(set) GenericEventBus $genericEventBus,
22+
public bool $preventHandling = true,
1523
) {}
1624

17-
public function listen(Closure $handler, ?string $event = null): void
25+
public function dispatch(string|object $event): void
1826
{
19-
$this->eventBusConfig->addClosureHandler($handler, $event);
27+
$this->dispatched[] = $event;
28+
29+
if ($this->preventHandling === false) {
30+
$this->genericEventBus->dispatch($event);
31+
}
2032
}
2133

22-
public function dispatch(string|object $event): void
34+
public function listen(Closure $handler, ?string $event = null): void
2335
{
24-
$this->dispatched[] = $event;
36+
$this->genericEventBus->listen($handler, $event);
2537
}
2638
}

0 commit comments

Comments
 (0)