Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Event Sourcing Phpunit Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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:

  1. Testing aggregates — the given/when/then aggregate test case.
  2. Testing subscribers — the setup/run/teardown utility for subscribers and projectors.

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.

Implementation Patterns

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.

Gotchas and Tips

  • Not a Laravel package. There is no service provider or Laravel-specific integration. It is a PHPUnit utility for the framework-agnostic core library. Do not expect RefreshDatabase, container resolution, or HTTP testing helpers.
  • Single inheritance conflict. If you extend the package’s aggregate test case, you cannot also extend Laravel’s 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.
  • Very low adoption. One star and zero dependents means few public examples and little community troubleshooting. It is first-party to the Patchlevel ecosystem, but pin the version and read the changelog before upgrading. Inspect vendor/patchlevel/event-sourcing-phpunit when docs and behaviour disagree.
  • API drift between versions. Base class names, configuration methods, and assertion signatures can change across major versions. Confirm the exact base class and method names for your installed version rather than copying an old blog snippet.
  • In-memory behaviour is not production behaviour. The test case likely uses an in-memory event store/message flow. It will not catch snapshot issues, optimistic-lock conflicts, real serialization problems, async projection lag, or database transaction bugs. Add integration tests for those.
  • Command dispatch requires #[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.
  • Event registration and serialization. Subscriber tests may need event classes registered with the event registry or serializer. Missing registration often appears as a serialization/deserialization error rather than an assertion failure.
  • Determinism. Inject fixed IDs and a controllable clock where possible. Avoid asserting generated UUIDs or real timestamps; compare stable fields or use the library’s clock abstraction if available.
  • Async subscribers are not exercised. A setup/run/teardown utility runs synchronously. It does not prove that a queued projector, outbox, or external transport works end-to-end.
  • Parallel test runs. Aggregate tests are usually safe under Laravel’s parallel testing. Subscriber tests that touch a database need isolated connections or RefreshDatabase; SQLite :memory: per process is a common choice.
  • Debugging failures. When 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.
  • Don’t over-assert state. If you assert every private field through closures, refactors become painful and the test duplicates implementation. Assert emitted events as the primary contract and only assert state that matters to callers.
  • Extension points. Use the test case’s overridable factory/configuration methods to supply custom event buses, clocks, or subscriber dependencies where supported. Keep those overrides in a small base test class so individual scenario tests stay declarative.
  • Pest users. This package is PHPUnit-centric. Class-based PHPUnit tests are the path of least resistance for aggregate scenarios; wrapping the base test case in Pest requires extra setup and may not be worth it for a small domain suite.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
ippanelcom/laravel-sdk
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus