From 49abb83352537a4ed6ad57537f35729961374b40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Re=CC=81mi=20Pelhate?= Date: Fri, 22 Aug 2025 12:04:26 +0200 Subject: [PATCH 1/6] =?UTF-8?q?Don=E2=80=99t=20use=20variadic=20argument?= =?UTF-8?q?=20on=20SpyCallable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This causes issues when using it in PHPUnit’s Callback constraint which will try to unpack any argument for callbacks with variadic arguments. --- src/PHPUnit/Doubles/SpyCallable.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/PHPUnit/Doubles/SpyCallable.php b/src/PHPUnit/Doubles/SpyCallable.php index 2258905..77622e5 100644 --- a/src/PHPUnit/Doubles/SpyCallable.php +++ b/src/PHPUnit/Doubles/SpyCallable.php @@ -7,6 +7,7 @@ use Craftzing\TestBench\PHPUnit\AssertsConstraints; use function call_user_func_array; +use function func_get_args; use function is_callable; final class SpyCallable @@ -22,8 +23,9 @@ public function __construct( public readonly mixed $return = null, ) {} - public function __invoke(mixed ...$arguments): mixed + public function __invoke(): mixed { + $arguments = func_get_args(); $this->invocations[] = new CallableInvocation(...$arguments); if (is_callable($this->return)) { From 6da86e258444119dd1d5cc0adbeb86dab11f7591 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Re=CC=81mi=20Pelhate?= Date: Fri, 22 Aug 2025 12:05:44 +0200 Subject: [PATCH 2/6] Add Spy constraint to enable inspecting constraint invocations --- src/PHPUnit/Constraint/Spy.php | 35 ++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/PHPUnit/Constraint/Spy.php diff --git a/src/PHPUnit/Constraint/Spy.php b/src/PHPUnit/Constraint/Spy.php new file mode 100644 index 0000000..a82a42c --- /dev/null +++ b/src/PHPUnit/Constraint/Spy.php @@ -0,0 +1,35 @@ +matches->__invoke($other); + } + + public function toString(): string + { + return 'constraint matches as Spy was configured to fail'; + } +} From 52f5075103c335593d31e29563a49f4110632fc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Re=CC=81mi=20Pelhate?= Date: Fri, 22 Aug 2025 12:21:25 +0200 Subject: [PATCH 3/6] Enable spying on DeriveConstraintsFromObjectUsingFakes --- .../Objects/DeriveConstraintsFromObjectUsingFakes.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/PHPUnit/Constraint/Objects/DeriveConstraintsFromObjectUsingFakes.php b/src/PHPUnit/Constraint/Objects/DeriveConstraintsFromObjectUsingFakes.php index baf91fa..e2d6dbb 100644 --- a/src/PHPUnit/Constraint/Objects/DeriveConstraintsFromObjectUsingFakes.php +++ b/src/PHPUnit/Constraint/Objects/DeriveConstraintsFromObjectUsingFakes.php @@ -4,16 +4,21 @@ namespace Craftzing\TestBench\PHPUnit\Constraint\Objects; +use Craftzing\TestBench\PHPUnit\Doubles\SpyCallable; use PHPUnit\Framework\Constraint\Callback; final readonly class DeriveConstraintsFromObjectUsingFakes implements DeriveConstraintsFromObject { + public SpyCallable $invoke; + /** * @param array<\PHPUnit\Framework\Constraint\Constraint> $constraints */ public function __construct( public array $constraints, - ) {} + ) { + $this->invoke = new SpyCallable(); + } public static function failingConstraints(): self { @@ -31,6 +36,8 @@ public static function passingConstraints(): self public function __invoke(object $object): array { + $this->invoke->__invoke($object); + return $this->constraints; } } From 78b77194d9d57fbe0cdb5b1992e7a2ae82da8d17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Re=CC=81mi=20Pelhate?= Date: Fri, 22 Aug 2025 12:22:27 +0200 Subject: [PATCH 4/6] Confirm issue with test --- src/Laravel/Constraint/Bus/WasHandledTest.php | 34 ++++++++++++++++++- .../Constraint/Callables/WasCalled.php | 12 +++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/Laravel/Constraint/Bus/WasHandledTest.php b/src/Laravel/Constraint/Bus/WasHandledTest.php index 73a9b7c..cd13903 100644 --- a/src/Laravel/Constraint/Bus/WasHandledTest.php +++ b/src/Laravel/Constraint/Bus/WasHandledTest.php @@ -4,8 +4,10 @@ namespace Craftzing\TestBench\Laravel\Constraint\Bus; +use Craftzing\TestBench\PHPUnit\Constraint\Callables\WasCalled; use Craftzing\TestBench\PHPUnit\Constraint\Objects\DeriveConstraintsFromObjectUsingFakes; use Craftzing\TestBench\PHPUnit\Constraint\Objects\DeriveConstraintsFromObjectUsingReflection; +use Craftzing\TestBench\PHPUnit\Constraint\Spy; use Craftzing\TestBench\PHPUnit\DataProviders\QuantableConstraint; use Craftzing\TestBench\PHPUnit\Doubles\SpyCallable; use Illuminate\Support\Facades\Bus; @@ -269,7 +271,7 @@ public function itCanDeriveCommandConstraintsFromCommandObjectsUsingCustomImplem } #[Test] - public function itFailsWhenNotHandledWithDerivedCommandConstraints(): void + public function itFailsWhenHandledButNotWithDerivedCommandConstraints(): void { $command = new stdClass(); WasHandled::using(fn (stdClass $command): string => 'handled', $this->app); @@ -293,4 +295,34 @@ public function itPassesWhenDispatchedWithDerivedCommandConstraints(): void $this->assertThat($command, new WasHandled()); } + + #[Test] + public function itDerivesConstraintsFromExpectedCommandsAndMatchesItAgainstActualCommands(): void + { + $expected = $this->command(['id' => 'expected']); + $actual = $this->command(['id' => 'actual']); + $constraint = Spy::passing(); + $deriveConstraints = new DeriveConstraintsFromObjectUsingFakes([$constraint]); + WasHandled::deriveConstraintsFromObjectUsing($deriveConstraints); + WasHandled::using(fn (stdClass $command): string => 'handled', $this->app); + Bus::dispatch($actual); + + $this->assertThat($expected, new WasHandled()); + + $deriveConstraints->invoke->assert(new WasCalled()->withSame($expected)); + $deriveConstraints->invoke->assert(new WasCalled()->never()->withSame($actual)); + $constraint->matches->assert(new WasCalled()->withSame($actual)); + $constraint->matches->assert(new WasCalled()->never()->withSame($expected)); + } + + private function command(array $properties): stdClass + { + $command = new stdClass(); + + foreach ($properties as $property => $value) { + $command->{$property} = $value; + } + + return $command; + } } diff --git a/src/PHPUnit/Constraint/Callables/WasCalled.php b/src/PHPUnit/Constraint/Callables/WasCalled.php index 694faef..396d82b 100644 --- a/src/PHPUnit/Constraint/Callables/WasCalled.php +++ b/src/PHPUnit/Constraint/Callables/WasCalled.php @@ -11,6 +11,7 @@ use Craftzing\TestBench\PHPUnit\Doubles\SpyCallable; use InvalidArgumentException; use Override; +use PHPUnit\Framework\Assert; use PHPUnit\Framework\Constraint\Constraint; use PHPUnit\Framework\ExpectationFailedException; @@ -26,6 +27,17 @@ public function __construct( public readonly ?int $times = null, ) {} + public function withSame(mixed ...$expected): self + { + return new self(function (mixed ...$actual) use ($expected): void { + Assert::assertCount(count($expected), $actual); + + foreach ($actual as $key => $value) { + Assert::assertSame($expected[$key], $value); + } + }, $this->times); + } + public function times(int $count): self { return new self($this->assertInvocation, $count); From eff17be034400bd6e122385822fafd4238a5b188 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Re=CC=81mi=20Pelhate?= Date: Fri, 22 Aug 2025 12:30:25 +0200 Subject: [PATCH 5/6] Derive constraints from expected, but match against actual --- src/Laravel/Constraint/Bus/WasHandled.php | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/Laravel/Constraint/Bus/WasHandled.php b/src/Laravel/Constraint/Bus/WasHandled.php index 51e0644..a1295dd 100644 --- a/src/Laravel/Constraint/Bus/WasHandled.php +++ b/src/Laravel/Constraint/Bus/WasHandled.php @@ -78,13 +78,13 @@ protected function matches(mixed $other): bool default => $other, }; $handler = $this->handler($command); - $assertInvocation = match ($this->givenOrDerivedObjectConstraints($other)) { - [] => null, - default => $this->assertHandlerWasInvokedWithCommandConstraints(...), - }; try { - Assert::assertThat($handler, new WasCalled($assertInvocation, $this->times)); + $handler->assert(new WasCalled(function (object $handled) use ($command): void { + foreach ($this->givenOrDerivedObjectConstraints($command) as $constraint) { + Assert::assertThat($handled, $constraint); + } + }, $this->times)); } catch (ExpectationFailedException $expectationFailed) { $this->additionalFailureDescriptions[] = $expectationFailed->getMessage(); @@ -107,13 +107,6 @@ private function handler(object $command): SpyCallable return $handler; } - private function assertHandlerWasInvokedWithCommandConstraints(object $command): void - { - foreach ($this->givenOrDerivedObjectConstraints($command) as $constraint) { - Assert::assertThat($command, $constraint); - } - } - public function toString(): string { return 'command was handled'; From 52597d1423f9aac79ea3d4b4f778d3da64d41455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Re=CC=81mi=20Pelhate?= Date: Fri, 22 Aug 2025 12:31:58 +0200 Subject: [PATCH 6/6] Add missing coverage ignore --- src/Laravel/Constraint/Eloquent/ModelComparatorTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Laravel/Constraint/Eloquent/ModelComparatorTest.php b/src/Laravel/Constraint/Eloquent/ModelComparatorTest.php index f426a5a..0d5c82a 100644 --- a/src/Laravel/Constraint/Eloquent/ModelComparatorTest.php +++ b/src/Laravel/Constraint/Eloquent/ModelComparatorTest.php @@ -16,6 +16,9 @@ use function compact; +/** + * @codeCoverageIgnore + */ final class ModelComparatorTest extends TestCase { public static function accepts(): iterable