Tests
The library's design promotes easily testable code, and we offer several helpers to simplify the testing process even further. If you need additional support, we also provide a PHPUnit testing library to make testing even more convenient.
Testing with patchlevel/event-sourcing-phpunit
Aggregate Unit Tests
There is a special TestCase
for aggregate tests that you can extend. By extending AggregateRootTestCase
, you can use
the given/when/then notation, making the test's purpose clear. When extending this class, you must implement a method
that provides the fully qualified class name (FQCN) of the aggregate you want to test.
use Patchlevel\EventSourcing\PhpUnit\Test\AggregateRootTestCase;
final class ProfileTest extends AggregateRootTestCase
{
protected function aggregateClass(): string
{
return Profile::class;
}
public function testCreateProfile(): void
{
$this
->when(static fn () => Profile::createProfile(new CreateProfile(ProfileId::fromString('1'), Email::fromString('[email protected]'))))
->then(new ProfileCreated(ProfileId::fromString('1'), Email::fromString('[email protected]')));
}
}
use Patchlevel\EventSourcing\PhpUnit\Test\AggregateRootTestCase;
final class ProfileTest extends AggregateRootTestCase
{
// protected function aggregateClass(): string;
public function testBehaviour(): void
{
$this
->given(
new ProfileCreated(
ProfileId::fromString('1'),
Email::fromString('[email protected]'),
),
)
->when(static fn (Profile $profile) => $profile->visitProfile(ProfileId::fromString('2')))
->then(new ProfileVisited(ProfileId::fromString('2')));
}
}
Using Commandbus like syntax
When using the command bus and the #[Handle]
attributes in your aggregate, you can directly provide the command to the
when
method.
use Patchlevel\EventSourcing\PhpUnit\Test\AggregateRootTestCase;
final class ProfileTest extends AggregateRootTestCase
{
// protected function aggregateClass(): string;
public function testBehaviour(): void
{
$this
->when(new CreateProfile(ProfileId::fromString('1'), Email::fromString('[email protected]')))
->then(new ProfileCreated(ProfileId::fromString('1'), Email::fromString('[email protected]')));
}
}
when
.
In this example, a string is needed, which will be passed directly to the event.
use Patchlevel\EventSourcing\PhpUnit\Test\AggregateRootTestCase;
final class ProfileTest extends AggregateRootTestCase
{
// protected function aggregateClass(): string;
public function testBehaviour(): void
{
$this
->given(
new ProfileCreated(
ProfileId::fromString('1'),
Email::fromString('[email protected]'),
),
)
->when(
new VisitProfile(ProfileId::fromString('2')),
'Extra Parameter / Dependency',
)
->then(
new ProfileVisited(ProfileId::fromString('2'), 'Extra Parameter / Dependency'),
);
}
}
Subscriber Tests
For testing a subscriber, there is a utility class available. Using SubscriberUtilities
provides several DX features
that simplify testing.
First, you need to specify the subscriptions you want to test when initializing the utility class. Once set up, you can call three methods:
executeSetup
executeRun
executeTeardown
These methods automatically invoke the appropriate functions defined via attributes.
use Patchlevel\EventSourcing\PhpUnit\Test\SubscriberUtilities;
final class ProfileSubscriberTest extends TestCase
{
use SubscriberUtilities;
public function testProfileCreated(): void
{
$subscriber = new ProfileSubscriber(/* inject deps or mock tests as needed */);
$util = new SubscriberUtilities($subscriber);
$util->executeSetup();
$util->executeRun(
new ProfileCreated(
ProfileId::fromString('1'),
Email::fromString('[email protected]'),
),
);
$util->executeTeardown();
self::assertSame(3, $subscriber->count);
}
}
Tests with DateTime using a Clock
You should not instantiate the DateTimeImmutable
directly in the aggregate.
Instead, you should pass a Clock
to the aggregate and use this to get the current time.
This allows you to test the aggregate with a fixed time.
use Patchlevel\EventSourcing\Clock\FrozenClock;
use Patchlevel\EventSourcing\PhpUnit\Test\AggregateRootTestCase;
final class ProfileTest extends AggregateRootTestCase
{
// protected function aggregateClass(): string;
public function testCreateProfile(): void
{
$clock = new FrozenClock(new DateTimeImmutable('2021-01-01 00:00:00'));
$profile = Profile::createProfile(
ProfileId::generate(),
Email::fromString('[email protected]'),
$clock,
);
$clock->sleep(10);
$profile->changeEmail(Email::fromString('[email protected]'));
$this
->given(new ProfileCreated(ProfileId::fromString('1'), Email::fromString('[email protected]')))
->when(
new ChangeEmail(ProfileId::fromString('1'), Email::fromString('[email protected]')),
$clock,
)
->then(new EmailChanged(
ProfileId::fromString('1'),
Email::fromString('[email protected]'),
new DateTimeImmutable('2021-01-01 00:00:10'),
));
}
}
Note
You can find out more about the clock here.
Tip
You can use the FreezeClock in you integration tests to test the time-based behavior of your application.
Tests with UUID
Uuids are randomly generated and can be a problem in tests.
If you want deterministic tests, you can use the IncrementalRamseyUuidFactory
from the library.
use Patchlevel\EventSourcing\Test\IncrementalRamseyUuidFactory;
use PHPUnit\Framework\TestCase;
use Ramsey\Uuid\Uuid;
final class ProfileTest extends TestCase
{
public function setUp(): void
{
Uuid::setFactory(new IncrementalRamseyUuidFactory());
}
public function testCreateProfile(): void
{
$id1 = ProfileId::generate(); // 10000000-7000-0000-0000-000000000001
$id2 = ProfileId::generate(); // 10000000-7000-0000-0000-000000000002
}
}
Warning
The IncrementalRamseyUuidFactory
is only for testing purposes
and supports only the version 7 what is used by the library.