{"id":44871999,"url":"https://github.com/patchlevel/event-sourcing-phpunit","last_synced_at":"2026-02-17T13:14:31.838Z","repository":{"id":273914888,"uuid":"921236775","full_name":"patchlevel/event-sourcing-phpunit","owner":"patchlevel","description":"PHPUnit testing utilities for patchlevel/event-sourcing","archived":false,"fork":false,"pushed_at":"2026-02-15T04:33:23.000Z","size":452,"stargazers_count":0,"open_issues_count":5,"forks_count":1,"subscribers_count":2,"default_branch":"1.5.x","last_synced_at":"2026-02-15T16:31:51.960Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/patchlevel.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-01-23T15:41:25.000Z","updated_at":"2026-02-15T04:33:04.000Z","dependencies_parsed_at":"2025-02-15T15:18:03.975Z","dependency_job_id":"db27c2f3-3dfb-4979-94b3-4816bc69fd13","html_url":"https://github.com/patchlevel/event-sourcing-phpunit","commit_stats":null,"previous_names":["patchlevel/event-sourcing-phpunit"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/patchlevel/event-sourcing-phpunit","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patchlevel%2Fevent-sourcing-phpunit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patchlevel%2Fevent-sourcing-phpunit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patchlevel%2Fevent-sourcing-phpunit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patchlevel%2Fevent-sourcing-phpunit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/patchlevel","download_url":"https://codeload.github.com/patchlevel/event-sourcing-phpunit/tar.gz/refs/heads/1.5.x","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/patchlevel%2Fevent-sourcing-phpunit/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29545341,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-17T13:00:00.370Z","status":"ssl_error","status_checked_at":"2026-02-17T12:57:14.072Z","response_time":100,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-02-17T13:14:29.604Z","updated_at":"2026-02-17T13:14:31.831Z","avatar_url":"https://github.com/patchlevel.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![Mutation testing badge](https://img.shields.io/endpoint?style=flat\u0026url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fpatchlevel%2Fevent-sourcing-phpunit%2F1.0.x)](https://dashboard.stryker-mutator.io/reports/github.com/patchlevel/event-sourcing-phpunit/1.0.x)\n[![Latest Stable Version](https://poser.pugx.org/patchlevel/event-sourcing-phpunit/v)](//packagist.org/packages/patchlevel/event-sourcing-phpunit)\n[![License](https://poser.pugx.org/patchlevel/event-sourcing-phpunit/license)](//packagist.org/packages/patchlevel/event-sourcing-phpunit)\n\n# Testing utilities\n\nWith this library you can ease the testing for your [event-sourcing](https://github.com/patchlevel/event-sourcing)\nproject when using PHPUnit. It comes with utilities for aggregates and subscribers.\n\n## Installation\n\n```bash\ncomposer require --dev patchlevel/event-sourcing-phpunit\n```\n\n## Testing Aggregates\n\nThere is a special `TestCase` for aggregate tests which you can extend from. Extending from `AggregateRootTestCase`\nenables you to use the given / when / then notation. This makes it very clear what the test is doing. When extending the\nclass you will need to implement a method which provides the FQCN of the aggregate you want to test.\n\n```php\nfinal class ProfileTest extends AggregateRootTestCase\n{\n    protected function aggregateClass(): string\n    {\n        return Profile::class;\n    }\n}\n```\n\nWhen this is done, you already can start testing your behaviour. For example testing that a event is recorded.\n\n```php\nfinal class ProfileTest extends AggregateRootTestCase\n{\n    // protected function aggregateClass(): string;\n\n    public function testBehaviour(): void\n    {\n        $this\n            -\u003egiven(\n                new ProfileCreated(\n                    ProfileId::fromString('1'),\n                    Email::fromString('hq@patchlevel.de'),\n                ),\n            )\n            -\u003ewhen(static fn (Profile $profile) =\u003e $profile-\u003evisitProfile(ProfileId::fromString('2')))\n            -\u003ethen(new ProfileVisited(ProfileId::fromString('2')));\n    }\n}\n```\n\nYou can also provide multiple given events and expect multiple events:\n\n```php\nfinal class ProfileTest extends AggregateRootTestCase\n{\n    // protected function aggregateClass(): string;\n\n    public function testBehaviour(): void\n    {\n        $this\n            -\u003egiven(\n                new ProfileCreated(\n                    ProfileId::fromString('1'),\n                    Email::fromString('hq@patchlevel.de'),\n                ),\n                new ProfileVisited(ProfileId::fromString('2')),\n            )\n            -\u003ewhen(\n                static function (Profile $profile) {\n                    $profile-\u003evisitProfile(ProfileId::fromString('3'));\n                    $profile-\u003evisitProfile(ProfileId::fromString('4'));\n                }\n            )\n            -\u003ethen(\n                new ProfileVisited(ProfileId::fromString('3')),\n                new ProfileVisited(ProfileId::fromString('4')),\n            );\n    }\n}\n```\n\nYou can also test the creation of the aggregate:\n\n```php\nfinal class ProfileTest extends AggregateRootTestCase\n{\n    // protected function aggregateClass(): string;\n\n    public function testBehaviour(): void\n    {\n        $this\n            -\u003ewhen(static fn () =\u003e Profile::createProfile(ProfileId::fromString('1'), Email::fromString('hq@patchlevel.de')))\n            -\u003ethen(new ProfileCreated(ProfileId::fromString('1'), Email::fromString('hq@patchlevel.de')));\n    }\n}\n```\n\nAnd expect an exception and the message of it:\n\n```php\nfinal class ProfileTest extends AggregateRootTestCase\n{\n    // protected function aggregateClass(): string;\n\n    public function testBehaviour(): void\n    {\n        $this\n            -\u003egiven(\n                new ProfileCreated(\n                    ProfileId::fromString('1'),\n                    Email::fromString('hq@patchlevel.de'),\n                ),\n            )\n            -\u003ewhen(static fn (Profile $profile) =\u003e $profile-\u003ethrowException())\n            -\u003eexpectsException(ProfileError::class)\n            -\u003eexpectsExceptionMessage('throwing so that you can catch it!');\n    }\n}\n```\n\n### Asserting aggregate state\n\nYou can pass closures to `then()` to assert on the aggregate's state after the events have been applied. This is useful\nwhen your aggregate exposes state via public properties or getters that are set in `apply` methods. Closures receive the\naggregate instance and are executed after the event assertion. You can mix closures and expected events freely — event\norder is preserved regardless of callback placement.\n\n```php\nfinal class ProfileTest extends AggregateRootTestCase\n{\n    // protected function aggregateClass(): string;\n\n    public function testBehaviour(): void\n    {\n        $this\n            -\u003egiven(\n                new ProfileCreated(\n                    ProfileId::fromString('1'),\n                    Email::fromString('hq@patchlevel.de'),\n                ),\n            )\n            -\u003ewhen(static fn (Profile $profile) =\u003e $profile-\u003evisitProfile(ProfileId::fromString('2')))\n            -\u003ethen(\n                new ProfileVisited(ProfileId::fromString('2')),\n                static fn (Profile $profile) =\u003e self::assertSame('1', $profile-\u003eid()-\u003etoString()),\n            );\n    }\n}\n```\n\n\u003e [!NOTE]\n\u003e When `then()` receives only closures and no event objects, it strictly asserts that zero events were emitted.\n\n### Using Commandbus like syntax\n\nWhen using the command bus and the `#[Handle]` attributes in your aggregate you can also provide the command directly\nfor the `when` method.\n\n```php\nfinal class ProfileTest extends AggregateRootTestCase\n{\n    // protected function aggregateClass(): string;\n\n    public function testBehaviour(): void\n    {\n        $this\n            -\u003ewhen(new CreateProfile(ProfileId::fromString('1'), Email::fromString('hq@patchlevel.de')))\n            -\u003ethen(new ProfileCreated(ProfileId::fromString('1'), Email::fromString('hq@patchlevel.de')));\n    }\n}\n```\n\nIf more parameters than the command is needed, these can also be provided as additional parameters for `when`. In this\nexample the we need a string which will be directly passed to the event.\n\n```php\nfinal class ProfileTest extends AggregateRootTestCase\n{\n    // protected function aggregateClass(): string;\n\n    public function testBehaviour(): void\n    {\n        $this\n            -\u003egiven(\n                new ProfileCreated(\n                    ProfileId::fromString('1'),\n                    Email::fromString('hq@patchlevel.de'),\n                ),\n            )\n            -\u003ewhen(new VisitProfile(ProfileId::fromString('2')), 'Extra Parameter / Dependency')\n            -\u003ethen(new ProfileVisited(ProfileId::fromString('2'), 'Extra Parameter / Dependency'));\n    }\n}\n```\n\n## Testing Subscriber\n\nFor testing a subscriber there is a utility class which you can use. Using `SubscriberUtilities` will provide you a\nbunch of dx features which makes the testing easier. First, you will need to provide the utility class the subscriptions\nyou will want to test, this is done when initializing the class. After that, you can call these 3 methods:\n`executeSetup`, `executeRun` and `executeTeardown`. These methods will be calling the right methods which are defined\nvia the attributes. For our example we are taking as simplified subscriber:\n\n```php\nuse Patchlevel\\EventSourcing\\Attribute\\Setup;\nuse Patchlevel\\EventSourcing\\Attribute\\Subscribe;\nuse Patchlevel\\EventSourcing\\Attribute\\Subscriber;\nuse Patchlevel\\EventSourcing\\Attribute\\Teardown;\n\n#[Subscriber('profile_subscriber', RunMode::FromBeginning)]\nfinal class ProfileSubscriber\n{\n    public int $called = 0;\n\n    #[Subscribe(ProfileCreated::class)]\n    public function run(): void\n    {\n        $this-\u003ecalled++;\n    }\n\n    #[Setup]\n    public function setup(): void\n    {\n        $this-\u003ecalled++;\n    }\n\n    #[Teardown]\n    public function teardown(): void\n    {\n        $this-\u003ecalled++;\n    }\n}\n```\n\nWith this, we can now write our test for it:\n\n```php\nuse Patchlevel\\EventSourcing\\Attribute\\Subscriber;\nuse Patchlevel\\EventSourcing\\Subscription\\RunMode;\nuse Patchlevel\\EventSourcing\\PhpUnit\\Test\\SubscriberUtilities;\n\nfinal class ProfileSubscriberTest extends TestCase\n{\n    use SubscriberUtilities;\n\n    public function testProfileCreated(): void\n    {\n        $subscriber = new ProfileSubscriber(/* inject deps, if needed */);\n\n        $util = new SubscriberUtilities($subscriber);\n        $util-\u003eexecuteSetup();\n        $util-\u003eexecuteRun(\n            new ProfileCreated(\n                ProfileId::fromString('1'),\n                Email::fromString('hq@patchlevel.de'),\n            )\n        );\n       $util-\u003eexecuteTeardown();\n\n        self::assertSame(3, $subscriber-\u003ecount);\n    }\n}\n```\n\nThis Util class can be used for integration or unit tests.\n\nYou can also pass `Message` instances with additional headers to the `executeRun` method. This allows testing\nsubscribers that rely on additional parameters like header information:\n\n\n```php\nuse Patchlevel\\EventSourcing\\Attribute\\Subscribe;\nuse Patchlevel\\EventSourcing\\Attribute\\Subscriber;\nuse DateTimeImmutable;\n\n#[Subscriber('profile_subscriber', RunMode::FromBeginning)]\nfinal class ProfileSubscriber\n{\n    #[Subscribe(ProfileCreated::class)]\n    public function run(ProfileCreated $event, DateTimeImmutable $recordedOn): void\n    {\n    }\n}\n```\n\nAdd any headers you want in the test:\n\n```php\nuse Patchlevel\\EventSourcing\\Attribute\\Subscriber;\nuse Patchlevel\\EventSourcing\\Message\\Message;\nuse Patchlevel\\EventSourcing\\Store\\Header\\RecordedOnHeader;\nuse Patchlevel\\EventSourcing\\Subscription\\RunMode;\nuse Patchlevel\\EventSourcing\\PhpUnit\\Test\\SubscriberUtilities;\nuse DateTimeImmutable;\n\nfinal class ProfileSubscriberTest extends TestCase\n{\n    use SubscriberUtilities;\n\n    public function testProfileCreated(): void\n    {\n        /* Setup and Teardown as before */\n\n        $util-\u003eexecuteRun(\n            Message::createWithHeaders(\n                new ProfileCreated(\n                    ProfileId::fromString('1'),\n                    Email::fromString('hq@patchlevel.de'),\n                ),\n                [new RecordedOnHeader(new DateTimeImmutable('now'))],\n            )\n        );\n\n       /* Your assertions */\n    }\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpatchlevel%2Fevent-sourcing-phpunit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpatchlevel%2Fevent-sourcing-phpunit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpatchlevel%2Fevent-sourcing-phpunit/lists"}