patchlevel/event-sourcing-phpunit
A dev testing toolkit for patchlevel/event-sourcing that lets you test aggregates and subscribers with clear given/when/then notation. Dispatch commands through #[Handle] methods, assert aggregate state with closures, and use subscriber setup, run, and teardown utilities.
Install it as a dev-only dependency alongside the core event-sourcing package:
composer require --dev patchlevel/event-sourcing-phpunit
Confirm the installed versions of patchlevel/event-sourcing, PHPUnit, and PHP are compatible; this package tracks the core library’s major line. In a Laravel app, keep event-sourcing domain tests outside the normal HTTP tests/Feature suite—typically tests/Unit/Domain/... or tests/Domain/...—because these tests exercise aggregates and subscribers directly, without booting a request lifecycle or hitting a database.
Look first at two documentation pages:
The first use case is a single aggregate command: given no history, when a create command is handled, then the expected domain event is recorded. The shape is typically:
use Patchlevel\EventSourcing\PhpUnit\Test\AggregateRootTestCase;
final class ProfileTest extends AggregateRootTestCase
{
protected function aggregateClass(): string
{
return Profile::class;
}
public function testProfileCanBeCreated(): void
{
$this->given()
->when(new CreateProfile('profile-1', 'David'))
->then(new ProfileCreated('profile-1', 'David'));
}
}
The exact base-class name and protected configuration method should be checked against the installed version’s source or docs. Run it with:
php artisan test --filter=ProfileTest
# or
vendor/bin/phpunit tests/Unit/Domain/ProfileTest.php
This gives you a fast, deterministic unit-test loop for event-sourced domain code before you add slower Laravel integration tests around persistence, projections, and message transports.
Scenario-style aggregate tests. Use given(...) to seed prior events, when(...) to invoke an aggregate method or dispatch a command through its #[Handle] method, and then(...) to assert the newly recorded events. This maps naturally to business rules: “given the profile already exists, when I rename it, then ProfileRenamed is recorded.” Cover both happy paths and rejected commands.
Command-handler coverage without a bus. Because when can dispatch commands through #[Handle] methods, you can test command-to-event behaviour without wiring Laravel’s container, a command bus, or a real event store. This is the highest-value day-to-day use: every new aggregate command gets a small scenario test.
State assertions with closures. After events are applied, assert aggregate state directly, e.g. that the name changed, the status is suspended, or a counter incremented. Use these sparingly alongside event assertions: events are the contract; state assertions are a convenient sanity check for reconstructed state.
Data-provider driven rule tables. For aggregates with several validation branches, use PHPUnit data providers to feed command/event tuples into one test. Keep each case small and name them by business rule rather than implementation detail.
Subscriber/projector tests. Use the subscriber utility’s setup, run, and teardown hooks to prepare a subscriber, feed it recorded messages/events, and assert the resulting read model. For Laravel projectors that write to Eloquent, either configure an in-memory SQLite connection and RefreshDatabase, or keep the subscriber utility for pure subscribers and use a Laravel integration test when container dependencies are required.
TDD workflow. Write the failing given/when/then scenario first, then implement the command handler and apply method. This is especially effective for bug fixes: reproduce the bug as a scenario with prior events, assert the correct event is emitted, then patch the aggregate.
Layered test strategy. Use this package for fast aggregate/subscriber unit tests, then add a smaller number of Laravel feature/integration tests that exercise the real event store, serialization, projections, and queue/transport configuration. Avoid trying to make the unit-test utility prove production persistence behaviour.
CI and mutation testing. These tests are cheap enough to run on every commit. If you already run mutation testing for domain code, aggregate scenarios make good mutation targets because they assert exact events and state transitions.
RefreshDatabase, container resolution, or HTTP testing helpers.Tests\TestCase. Prefer standalone domain tests for aggregates. For subscribers that need Laravel services, use the utility only if you can inject dependencies manually; otherwise write a Laravel test that boots the app.vendor/patchlevel/event-sourcing-phpunit when docs and behaviour disagree.#[Handle]. If when(new SomeCommand(...)) does not invoke your handler, verify the command has the correctly namespaced #[Handle] attribute and that the aggregate test case knows how to dispatch it. A plain command object with no handler will not behave like an aggregate method call.RefreshDatabase; SQLite :memory: per process is a common choice.then(...) fails, inspect the actual recorded event list and compare event order, aggregate IDs, and payload values. Event order is part of the contract; a missing intermediate event often explains a state assertion failure.How can I help you explore Laravel packages today?