Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add PoC for testing utilities: AggregateTestCase, SubscriberUtilities #670

Draft
wants to merge 4 commits into
base: 3.8.x
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,9 @@ parameters:
identifier: trait.unused
count: 1
path: src/Subscription/Subscriber/SubscriberUtil.php

-
message: '#^Trait Patchlevel\\EventSourcing\\Test\\SubscriberUtilities is used zero times and is not analysed\.$#'
identifier: trait.unused
count: 1
path: src/Test/SubscriberUtilities.php
13 changes: 13 additions & 0 deletions src/Test/AggregateAlreadySet.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Patchlevel\EventSourcing\Test;

final class AggregateAlreadySet extends AggregateTestError
{
public function __construct()
{
parent::__construct('Aggregate already set. You should only return the aggregate if there is no given present.');

Check warning on line 11 in src/Test/AggregateAlreadySet.php

View workflow job for this annotation

GitHub Actions / Mutation tests on diff (locked, 8.3, ubuntu-latest)

Escaped Mutant for Mutator "MethodCallRemoval": --- Original +++ New @@ @@ { public function __construct() { - parent::__construct('Aggregate already set. You should only return the aggregate if there is no given present.'); + } }
}
}
149 changes: 149 additions & 0 deletions src/Test/AggregateRootTestCase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
<?php

declare(strict_types=1);

namespace Patchlevel\EventSourcing\Test;

use Closure;
use Patchlevel\EventSourcing\Aggregate\AggregateRoot;
use PHPUnit\Framework\Attributes\After;
use PHPUnit\Framework\Attributes\Before;
use PHPUnit\Framework\Constraint\Exception as ExceptionConstraint;
use PHPUnit\Framework\Constraint\ExceptionMessageIsOrContains;
use PHPUnit\Framework\TestCase;
use Throwable;

abstract class AggregateRootTestCase extends TestCase
{
/** @var array<object> */
private array $givenEvents = [];

/** @var array<Closure> */
private array $whens = [];

/** @var array<object> */
private array $expectedEvents = [];
/** @var class-string<Throwable>|null */
private string|null $expectedException = null;
private string|null $expectedExceptionMessage = null;

/** @return class-string<AggregateRoot> */

Check failure on line 30 in src/Test/AggregateRootTestCase.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Deptrac (locked, 8.3, ubuntu-latest)

Patchlevel\EventSourcing\Test\AggregateRootTestCase must not depend on Patchlevel\EventSourcing\Aggregate\AggregateRoot (Test on Aggregate)
abstract protected function aggregateClass(): string;

public function given(object ...$events): self
{
$this->givenEvents = $events;

return $this;
}

public function when(Closure ...$callables): self
{
$this->whens = $callables;

return $this;
}

public function then(object ...$events): self
{
$this->expectedEvents = $events;

return $this;
}

/** @param class-string<Throwable> $exception */
public function expectsException(string $exception): self
{
$this->expectedException = $exception;

return $this;
}

public function expectsExceptionMessage(string $exceptionMessage): self
{
$this->expectedExceptionMessage = $exceptionMessage;

return $this;
}

#[After]
public function assert(): self
{
$aggregate = null;

if ($this->givenEvents) {
$aggregate = $this->aggregateClass()::createFromEvents($this->givenEvents);
}

try {
foreach ($this->whens as $callable) {
$return = $callable($aggregate);

if ($aggregate !== null && $return instanceof AggregateRoot) {

Check failure on line 82 in src/Test/AggregateRootTestCase.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Deptrac (locked, 8.3, ubuntu-latest)

Patchlevel\EventSourcing\Test\AggregateRootTestCase must not depend on Patchlevel\EventSourcing\Aggregate\AggregateRoot (Test on Aggregate)
throw new AggregateAlreadySet();
}

if ($aggregate === null && !$return instanceof AggregateRoot) {

Check failure on line 86 in src/Test/AggregateRootTestCase.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Deptrac (locked, 8.3, ubuntu-latest)

Patchlevel\EventSourcing\Test\AggregateRootTestCase must not depend on Patchlevel\EventSourcing\Aggregate\AggregateRoot (Test on Aggregate)
throw new NoAggregateCreated();
}

if ($aggregate !== null || !($return instanceof AggregateRoot)) {

Check failure on line 90 in src/Test/AggregateRootTestCase.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Deptrac (locked, 8.3, ubuntu-latest)

Patchlevel\EventSourcing\Test\AggregateRootTestCase must not depend on Patchlevel\EventSourcing\Aggregate\AggregateRoot (Test on Aggregate)
continue;
}

$aggregate = $return;
}
} catch (AggregateTestError $exception) {
throw $exception;
} catch (Throwable $throwable) {
$this->handleException($throwable);
}

if (!$aggregate instanceof AggregateRoot) {

Check failure on line 102 in src/Test/AggregateRootTestCase.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Deptrac (locked, 8.3, ubuntu-latest)

Patchlevel\EventSourcing\Test\AggregateRootTestCase must not depend on Patchlevel\EventSourcing\Aggregate\AggregateRoot (Test on Aggregate)
throw new NoAggregateCreated();
}

$events = $aggregate->releaseEvents();

self::assertEquals($this->expectedEvents, $events, 'The events doesn\'t match the expected events.');

return $this;
}

#[Before]
public function reset(): void
{
$this->givenEvents = [];
$this->whens = [];
$this->expectedEvents = [];
$this->expectedException = null;
$this->expectedExceptionMessage = null;
}

private function handleException(Throwable $throwable): void
{
if ($this->expectedException === null && $this->expectedExceptionMessage === null) {
throw $throwable;
}

if ($this->expectedException) {
$this->assertThat(
$throwable,
new ExceptionConstraint(

Check failure on line 132 in src/Test/AggregateRootTestCase.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)

InternalClass

src/Test/AggregateRootTestCase.php:132:17: InternalClass: PHPUnit\Framework\Constraint\Exception is internal to PHPUnit but called from Patchlevel\EventSourcing\Test\AggregateRootTestCase (see https://psalm.dev/174)

Check failure on line 132 in src/Test/AggregateRootTestCase.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)

InternalMethod

src/Test/AggregateRootTestCase.php:132:17: InternalMethod: Constructor PHPUnit\Framework\Constraint\Exception::__construct is internal to PHPUnit but called from Patchlevel\EventSourcing\Test\AggregateRootTestCase::handleException (see https://psalm.dev/175)
$this->expectedException,
),
);
}

if (!$this->expectedExceptionMessage) {

Check failure on line 138 in src/Test/AggregateRootTestCase.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)

RiskyTruthyFalsyComparison

src/Test/AggregateRootTestCase.php:138:13: RiskyTruthyFalsyComparison: Operand of type null|string contains type string, which can be falsy and truthy. This can cause possibly unexpected behavior. Use strict comparison instead. (see https://psalm.dev/356)
return;
}

$this->assertThat(
$throwable->getMessage(),
new ExceptionMessageIsOrContains(

Check failure on line 144 in src/Test/AggregateRootTestCase.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)

InternalClass

src/Test/AggregateRootTestCase.php:144:13: InternalClass: PHPUnit\Framework\Constraint\ExceptionMessageIsOrContains is internal to PHPUnit but called from Patchlevel\EventSourcing\Test\AggregateRootTestCase (see https://psalm.dev/174)

Check failure on line 144 in src/Test/AggregateRootTestCase.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)

InternalMethod

src/Test/AggregateRootTestCase.php:144:13: InternalMethod: Constructor PHPUnit\Framework\Constraint\ExceptionMessageIsOrContains::__construct is internal to PHPUnit but called from Patchlevel\EventSourcing\Test\AggregateRootTestCase::handleException (see https://psalm.dev/175)
$this->expectedExceptionMessage,
),
);
}
}
11 changes: 11 additions & 0 deletions src/Test/AggregateTestError.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace Patchlevel\EventSourcing\Test;

use RuntimeException;

abstract class AggregateTestError extends RuntimeException
{
}
13 changes: 13 additions & 0 deletions src/Test/NoAggregateCreated.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Patchlevel\EventSourcing\Test;

final class NoAggregateCreated extends AggregateTestError
{
public function __construct()
{
parent::__construct('No aggregate set and no aggregate returned. Please provide given events or create the aggregate with the first action.');

Check warning on line 11 in src/Test/NoAggregateCreated.php

View workflow job for this annotation

GitHub Actions / Mutation tests on diff (locked, 8.3, ubuntu-latest)

Escaped Mutant for Mutator "MethodCallRemoval": --- Original +++ New @@ @@ { public function __construct() { - parent::__construct('No aggregate set and no aggregate returned. Please provide given events or create the aggregate with the first action.'); + } }
}
}
91 changes: 91 additions & 0 deletions src/Test/SubscriberUtilities.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

declare(strict_types=1);

namespace Patchlevel\EventSourcing\Test;

use Patchlevel\EventSourcing\Message\Message;
use Patchlevel\EventSourcing\Subscription\Subscriber\MetadataSubscriberAccessor;
use Patchlevel\EventSourcing\Subscription\Subscriber\MetadataSubscriberAccessorRepository;
use PHPUnit\Framework\Attributes\Before;

trait SubscriberUtilities
{
/** @var array<object> */
private array $givenEvents = [];
/** @var MetadataSubscriberAccessor[] */

Check failure on line 16 in src/Test/SubscriberUtilities.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Deptrac (locked, 8.3, ubuntu-latest)

Patchlevel\EventSourcing\Test\SubscriberUtilities must not depend on Patchlevel\EventSourcing\Subscription\Subscriber\MetadataSubscriberAccessor (Test on Subscription)
private iterable $subscriberAccessors;

Check failure on line 17 in src/Test/SubscriberUtilities.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)

MissingConstructor

src/Test/SubscriberUtilities.php:17:22: MissingConstructor: Patchlevel\EventSourcing\Tests\Unit\Test\_home_runner_work_event_sourcing_event_sourcing_tests_Unit_Test_SubscriberUtilitiesTest_php_146_3568 has an uninitialized property Patchlevel\EventSourcing\Test\SubscriberUtilities::$subscriberAccessors, but no constructor (see https://psalm.dev/073)

public function given(object ...$events): self
{
$this->givenEvents = $events;

return $this;
}

public function executeSetup(object ...$subscribers): self
{
$subscriberAccessors = $this->createSubscriberAccessors($subscribers);

foreach ($subscriberAccessors as $subscriberAccessor) {
$setupMethod = $subscriberAccessor->setupMethod();

if (!$setupMethod) {
continue;

Check warning on line 34 in src/Test/SubscriberUtilities.php

View workflow job for this annotation

GitHub Actions / Mutation tests on diff (locked, 8.3, ubuntu-latest)

Escaped Mutant for Mutator "Continue_": --- Original +++ New @@ @@ foreach ($subscriberAccessors as $subscriberAccessor) { $setupMethod = $subscriberAccessor->setupMethod(); if (!$setupMethod) { - continue; + break; } $setupMethod(); }
}

$setupMethod();
}

return $this;
}

public function executeRun(object ...$subscribers): self
{
$subscriberAccessors = $this->createSubscriberAccessors($subscribers);

foreach ($this->givenEvents as $event) {
foreach ($subscriberAccessors as $subscriberAccessor) {
foreach ($subscriberAccessor->subscribeMethods($event::class) as $subscribeMethod) {
$subscribeMethod(Message::create($event));

Check failure on line 50 in src/Test/SubscriberUtilities.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Deptrac (locked, 8.3, ubuntu-latest)

Patchlevel\EventSourcing\Test\SubscriberUtilities must not depend on Patchlevel\EventSourcing\Message\Message (Test on Message)
}
}
}

return $this;
}

public function executeTeardown(object ...$subscribers): self
{
$subscriberAccessors = $this->createSubscriberAccessors($subscribers);

foreach ($subscriberAccessors as $subscriberAccessor) {
$teardownMethod = $subscriberAccessor->teardownMethod();

if (!$teardownMethod) {
continue;

Check warning on line 66 in src/Test/SubscriberUtilities.php

View workflow job for this annotation

GitHub Actions / Mutation tests on diff (locked, 8.3, ubuntu-latest)

Escaped Mutant for Mutator "Continue_": --- Original +++ New @@ @@ foreach ($subscriberAccessors as $subscriberAccessor) { $teardownMethod = $subscriberAccessor->teardownMethod(); if (!$teardownMethod) { - continue; + break; } $teardownMethod(); }
}

$teardownMethod();
}

return $this;
}

#[Before]
public function reset(): void
{
$this->givenEvents = [];
unset($this->subscriberAccessors);
}

/**

Check failure on line 82 in src/Test/SubscriberUtilities.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Deptrac (locked, 8.3, ubuntu-latest)

Patchlevel\EventSourcing\Test\SubscriberUtilities must not depend on Patchlevel\EventSourcing\Subscription\Subscriber\MetadataSubscriberAccessor (Test on Subscription)
* @param array<object> $subscribers
*
* @return iterable<MetadataSubscriberAccessor>
*/
private function createSubscriberAccessors(array $subscribers): iterable
{
return $this->subscriberAccessors ??= (new MetadataSubscriberAccessorRepository($subscribers))->all();

Check failure on line 89 in src/Test/SubscriberUtilities.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Deptrac (locked, 8.3, ubuntu-latest)

Patchlevel\EventSourcing\Test\SubscriberUtilities must not depend on Patchlevel\EventSourcing\Subscription\Subscriber\MetadataSubscriberAccessorRepository (Test on Subscription)

Check failure on line 89 in src/Test/SubscriberUtilities.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)

RedundantPropertyInitializationCheck

src/Test/SubscriberUtilities.php:89:16: RedundantPropertyInitializationCheck: Property $this->subscriberAccessors with type array<array-key, Patchlevel\EventSourcing\Subscription\Subscriber\MetadataSubscriberAccessor> should already be set in the constructor (see https://psalm.dev/261)

Check failure on line 89 in src/Test/SubscriberUtilities.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)

InvalidPropertyAssignmentValue

src/Test/SubscriberUtilities.php:89:16: InvalidPropertyAssignmentValue: $this->subscriberAccessors with declared type 'array<array-key, Patchlevel\EventSourcing\Subscription\Subscriber\MetadataSubscriberAccessor>' cannot be assigned type 'iterable<array-key|mixed, Patchlevel\EventSourcing\Subscription\Subscriber\MetadataSubscriberAccessor|Patchlevel\EventSourcing\Subscription\Subscriber\MetadataSubscriberAccessor<object>>' (see https://psalm.dev/145)

Check failure on line 89 in src/Test/SubscriberUtilities.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Psalm (locked, 8.3, ubuntu-latest)

RedundantPropertyInitializationCheck

src/Test/SubscriberUtilities.php:89:47: RedundantPropertyInitializationCheck: Property $this->subscriberAccessors with type array<array-key, Patchlevel\EventSourcing\Subscription\Subscriber\MetadataSubscriberAccessor> should already be set in the constructor (see https://psalm.dev/261)

Check warning on line 89 in src/Test/SubscriberUtilities.php

View workflow job for this annotation

GitHub Actions / Mutation tests on diff (locked, 8.3, ubuntu-latest)

Escaped Mutant for Mutator "AssignCoalesce": --- Original +++ New @@ @@ */ private function createSubscriberAccessors(array $subscribers) : iterable { - return $this->subscriberAccessors ??= (new MetadataSubscriberAccessorRepository($subscribers))->all(); + return $this->subscriberAccessors = (new MetadataSubscriberAccessorRepository($subscribers))->all(); } }
}
}
6 changes: 6 additions & 0 deletions tests/Unit/Fixture/Profile.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Patchlevel\EventSourcing\Attribute\Apply;
use Patchlevel\EventSourcing\Attribute\Id;
use Patchlevel\EventSourcing\Attribute\SuppressMissingApply;
use RuntimeException;

#[Aggregate('profile')]
#[SuppressMissingApply([MessageDeleted::class])]
Expand Down Expand Up @@ -66,6 +67,11 @@ public function splitIt(): void
$this->recordThat(new SplittingEvent($this->email, $this->visits));
}

public function throwException(): void
{
throw new RuntimeException('throwing so that you can catch it!');
}

#[Apply(ProfileCreated::class)]
#[Apply(ProfileVisited::class)]
protected function applyProfileCreated(ProfileCreated|ProfileVisited $event): void
Expand Down
Loading
Loading