From bd8938229d6cc020bef368869ce768fc05515242 Mon Sep 17 00:00:00 2001 From: Daniel Badura Date: Thu, 2 Apr 2026 13:45:10 +0200 Subject: [PATCH] Move execution into `when` instead of the `#[After]` hook. This enables unit testing with phpunit mocks, as they are currently cleanuped before asserting. --- src/Test/AggregateRootTestCase.php | 83 ++++++++++--------- tests/Unit/Fixture/Notifier.php | 10 +++ tests/Unit/Fixture/Profile.php | 6 +- tests/Unit/Test/AggregateRootTestCaseTest.php | 41 ++++----- 4 files changed, 78 insertions(+), 62 deletions(-) create mode 100644 tests/Unit/Fixture/Notifier.php diff --git a/src/Test/AggregateRootTestCase.php b/src/Test/AggregateRootTestCase.php index 0d3716e..1ecb976 100644 --- a/src/Test/AggregateRootTestCase.php +++ b/src/Test/AggregateRootTestCase.php @@ -28,10 +28,12 @@ abstract class AggregateRootTestCase extends TestCase /** @var array */ private array $parameters = []; + private object|null $aggregate = null; /** @var array */ private array $thenExpectations = []; + private Throwable|null $thrownException = null; /** @var class-string|null */ private string|null $expectedException = null; private string|null $expectedExceptionMessage = null; @@ -41,6 +43,7 @@ abstract protected function aggregateClass(): string; final public function given(object ...$events): self { + $this->aggregate = null; $this->givenEvents = $events; return $this; @@ -52,6 +55,42 @@ final public function when(object $callable, mixed ...$parameters): self $this->when = $callable; $this->parameters = $parameters; + if ($this->givenEvents) { + $this->aggregate = $this->aggregateClass()::createFromEvents($this->givenEvents); + } + + try { + $callableOrCommand = $this->when; + $return = null; + + if ($callableOrCommand instanceof Closure) { + /** @var AggregateRoot|null $return */ + $return = $callableOrCommand($this->aggregate); + } else { + foreach (HandlerFinder::findInClass($this->aggregateClass()) as $handler) { + if (!$callableOrCommand instanceof $handler->commandClass) { + continue; + } + + $reflection = new ReflectionClass($this->aggregateClass()); + $reflectionMethod = $reflection->getMethod($handler->method); + + /** @var AggregateRoot|null $return */ + $return = $reflectionMethod->invokeArgs($handler->static ? null : $this->aggregate, [$callableOrCommand, ...$this->parameters]); + } + } + + if ($this->aggregate !== null && $return instanceof AggregateRoot) { + throw new AggregateAlreadySet(); + } + + if ($this->aggregate === null) { + $this->aggregate = $return; + } + } catch (Throwable $throwable) { + $this->thrownException = $throwable; + } + return $this; } @@ -89,60 +128,28 @@ final public function assert(): self throw new NoWhenProvided(); } - $aggregate = null; - - if ($this->givenEvents) { - $aggregate = $this->aggregateClass()::createFromEvents($this->givenEvents); - } - - try { - $callableOrCommand = $this->when; - $return = null; - - if ($callableOrCommand instanceof Closure) { - $return = $callableOrCommand($aggregate); - } else { - foreach (HandlerFinder::findInClass($this->aggregateClass()) as $handler) { - if (!$callableOrCommand instanceof $handler->commandClass) { - continue; - } - - $reflection = new ReflectionClass($this->aggregateClass()); - $reflectionMethod = $reflection->getMethod($handler->method); - - $return = $reflectionMethod->invokeArgs($handler->static ? null : $aggregate, [$callableOrCommand, ...$this->parameters]); - } - } - - if ($aggregate !== null && $return instanceof AggregateRoot) { - throw new AggregateAlreadySet(); - } - - if ($aggregate === null) { - $aggregate = $return; - } - } catch (Throwable $throwable) { - $this->handleException($throwable); + if ($this->thrownException !== null) { + $this->handleException($this->thrownException); return $this; } $this->expectedExceptionWasNotThrown(); - if (!$aggregate instanceof AggregateRoot) { + if (!$this->aggregate instanceof AggregateRoot) { throw new NoAggregateCreated(); } $expectedEvents = array_values(array_filter($this->thenExpectations, static fn (object $item) => !$item instanceof Closure)); $expectationCallbacks = array_filter($this->thenExpectations, static fn (object $item) => $item instanceof Closure); - $events = $aggregate->releaseEvents(); + $events = $this->aggregate->releaseEvents(); self::assertEquals($expectedEvents, $events, 'The events doesn\'t match the expected events.'); foreach ($expectationCallbacks as $callback) { try { - $callback($aggregate); + $callback($this->aggregate); } catch (Throwable $t) { $reflection = new ReflectionFunction($callback); @@ -169,6 +176,8 @@ final public function reset(): void $this->thenExpectations = []; $this->expectedException = null; $this->expectedExceptionMessage = null; + $this->aggregate = null; + $this->thrownException = null; } private function handleException(Throwable $throwable): void diff --git a/tests/Unit/Fixture/Notifier.php b/tests/Unit/Fixture/Notifier.php new file mode 100644 index 0000000..7aa9a00 --- /dev/null +++ b/tests/Unit/Fixture/Notifier.php @@ -0,0 +1,10 @@ +recordThat(new ProfileCreated($createProfile->id, $createProfile->email)); + if ($notifier) { + $notifier->notify(); + } + return $self; } diff --git a/tests/Unit/Test/AggregateRootTestCaseTest.php b/tests/Unit/Test/AggregateRootTestCaseTest.php index f30c290..08b8c69 100644 --- a/tests/Unit/Test/AggregateRootTestCaseTest.php +++ b/tests/Unit/Test/AggregateRootTestCaseTest.php @@ -13,6 +13,7 @@ use Patchlevel\EventSourcing\PhpUnit\Tests\Unit\Fixture\CreateProfile; use Patchlevel\EventSourcing\PhpUnit\Tests\Unit\Fixture\CreateProfileWithFailure; use Patchlevel\EventSourcing\PhpUnit\Tests\Unit\Fixture\Email; +use Patchlevel\EventSourcing\PhpUnit\Tests\Unit\Fixture\Notifier; use Patchlevel\EventSourcing\PhpUnit\Tests\Unit\Fixture\Profile; use Patchlevel\EventSourcing\PhpUnit\Tests\Unit\Fixture\ProfileCreated; use Patchlevel\EventSourcing\PhpUnit\Tests\Unit\Fixture\ProfileError; @@ -232,29 +233,6 @@ public function testVisited(): void self::assertSame(1, $test::getCount()); } - public function testVisitedDoubleAssert(): void - { - $test = $this->getTester(); - - $test - ->given( - new ProfileCreated( - ProfileId::fromString('1'), - Email::fromString('hq@patchlevel.de'), - ), - ) - ->when( - static fn (Profile $profile) => $profile->visitProfile(new VisitProfile(ProfileId::fromString('2'))), - ) - ->then( - new ProfileVisited(ProfileId::fromString('2')), - ); - - $test->assert(); - $test->assert(); - self::assertSame(2, $test::getCount()); - } - public function testCreation(): void { $test = $this->getTester(); @@ -310,7 +288,7 @@ public function testNoGivenAndNoCreation(): void $test ->when( - static fn () => 'no aggregate as return', + static fn () => null, ) ->then( new ProfileVisited(ProfileId::fromString('2')), @@ -344,6 +322,21 @@ public function testDoubleAggregateCreation(): void self::assertSame(1, $test::getCount()); } + public function testCreationWithMock(): void + { + $test = $this->getTester(); + + $notifier = $this->createMock(Notifier::class); + $notifier->expects($this->once())->method('notify'); + + $test + ->when(new CreateProfile(ProfileId::fromString('1'), Email::fromString('hq@patchlevel.de')), $notifier) + ->then(new ProfileCreated(ProfileId::fromString('1'), Email::fromString('hq@patchlevel.de'))); + + $test->assert(); + self::assertSame(1, $test::getCount()); + } + public function testReset(): void { $test = $this->getTester();