patchlevel / event-sourcing-phpunit
PHPUnit testing utilities for patchlevel/event-sourcing
Requires
- php: ~8.1.0 || ~8.2.0 || ~8.3.0
- patchlevel/event-sourcing: ^3.0.0
- phpunit/phpunit: ^10.1.0||^11.0.0
Requires (Dev)
- infection/infection: ^0.29.0
- patchlevel/coding-standard: ^1.3.0
- phpstan/phpstan: ^2.1.0
This package is auto-updated.
Last update: 2025-01-27 07:18:31 UTC
README
Testing utilities
With this library you can ease the testing for your event-sourcing project when using PHPUnit. It comes with utilities for aggregates and subscribers.
Installation
composer require --dev patchlevel/event-sourcing-phpunit
Given / When / Then
Testing Aggregates
There is a special TestCase
for aggregate tests which you can extend from. Extending from AggregateRootTestCase
enables you to use the given / when / then notation. This makes it very clear what the test is doing. When extending the
class you will need to implement a method which provides the FQCN of the aggregate you want to test.
final class ProfileTest extends AggregateRootTestCase { protected function aggregateClass(): string { return Profile::class; } }
When this is done, you already can start testing your behaviour. For example testing that a event is recorded.
final class ProfileTest extends AggregateRootTestCase { // protected function aggregateClass(): string; public function testBehaviour(): void { $this ->given( new ProfileCreated( ProfileId::fromString('1'), Email::fromString('hq@patchlevel.de'), ), ) ->when(static fn (Profile $profile) => $profile->visitProfile(ProfileId::fromString('2'))) ->then(new ProfileVisited(ProfileId::fromString('2'))); } }
You can also test multiple calls and events:
final class ProfileTest extends AggregateRootTestCase { // protected function aggregateClass(): string; public function testBehaviour(): void { $this ->given( new ProfileCreated( ProfileId::fromString('1'), Email::fromString('hq@patchlevel.de'), ), ) ->when( static fn (Profile $profile) => $profile->visitProfile(ProfileId::fromString('2')), static fn (Profile $profile) => $profile->visitProfile(ProfileId::fromString('2')), static fn (Profile $profile) => $profile->visitProfile(ProfileId::fromString('2')), ) ->then( new ProfileVisited(ProfileId::fromString('2')), new ProfileVisited(ProfileId::fromString('2')), new ProfileVisited(ProfileId::fromString('2')), ); } }
You can also test the creation of the aggregate:
final class ProfileTest extends AggregateRootTestCase { // protected function aggregateClass(): string; public function testBehaviour(): void { $this ->when(static fn () => Profile::createProfile(ProfileId::fromString('1'), Email::fromString('hq@patchlevel.de'))) ->then(new ProfileCreated(ProfileId::fromString('1'), Email::fromString('hq@patchlevel.de'))); } }
And expect an exception and the message of it:
final class ProfileTest extends AggregateRootTestCase { // protected function aggregateClass(): string; public function testBehaviour(): void { $this ->given( new ProfileCreated( ProfileId::fromString('1'), Email::fromString('hq@patchlevel.de'), ), ) ->when(static fn (Profile $profile) => $profile->throwException()) ->expectsException(ProfileError::class) ->expectsExceptionMessage('throwing so that you can catch it!'); } }
Testing Subscriber
For testing a subscriber there is a trait which you can use. When using SubscriberUtilities
you will also be provided
with a bunch of dx features which makes the testing easier. First, providing the events is the same with a given
method. After that, you can call executeRun
which can take multiple subscribers, which will be provided with the
given
events. The events will be mapped according the #[Subscribe]
attribute. For our example we are taking as
simplified subscriber:
use Patchlevel\EventSourcing\Attribute\Setup; use Patchlevel\EventSourcing\Attribute\Subscribe; use Patchlevel\EventSourcing\Attribute\Subscriber; use Patchlevel\EventSourcing\Attribute\Teardown; #[Subscriber('profile_subscriber', RunMode::FromBeginning)] final class ProfileSubscriber { public int $called = 0; #[Subscribe(ProfileCreated::class)] public function run(): void { $this->called++; } #[Setup] public function setup(): void { $this->called++; } #[Teardown] public function teardown(): void { $this->called++; } }
With this, we can now write our test for it:
use Patchlevel\EventSourcing\Attribute\Subscriber; use Patchlevel\EventSourcing\Subscription\RunMode; use Patchlevel\EventSourcing\PhpUnit\Test\SubscriberUtilities; final class ProfileSubscriberTest extends TestCase { use SubscriberUtilities; public function testProfileCreated(): void { $subscriber = new ProfileSubscriber(/* inject deps, if needed */); $this ->given( new ProfileCreated( ProfileId::fromString('1'), Email::fromString('hq@patchlevel.de'), ), ) ->executeRun($subscriber); self::assertSame(1, $subscriber->count); } }
You can also test the setup and teardown methods:
use Patchlevel\EventSourcing\Attribute\Subscriber; use Patchlevel\EventSourcing\Subscription\RunMode; use Patchlevel\EventSourcing\PhpUnit\Test\SubscriberUtilities; final class ProfileSubscriberTest extends TestCase { use SubscriberUtilities; public function testSetup(): void { $subscriber = new ProfileSubscriber(/* inject deps, if needed */); $this->executeSetup($subscriber); self::assertSame(1, $subscriber->count); } public function testTeardown(): void { $subscriber = new ProfileSubscriber(/* inject deps, if needed */); $this->executeTeardown($subscriber); self::assertSame(1, $subscriber->count); } }
Of course, you can also execute the whole workflow in one test:
use Patchlevel\EventSourcing\Attribute\Subscriber; use Patchlevel\EventSourcing\Subscription\RunMode; use Patchlevel\EventSourcing\PhpUnit\Test\SubscriberUtilities; final class ProfileSubscriberTest extends TestCase { use SubscriberUtilities; public function testProfileCreated(): void { $subscriber = new ProfileSubscriber(/* inject deps, if needed */); $this ->given( new ProfileCreated( ProfileId::fromString('1'), Email::fromString('hq@patchlevel.de'), ), ) ->executeSetup($subscriber) ->executeRun($subscriber) ->executeTeardown($subscriber); self::assertSame(3, $subscriber->count); } }