diff --git a/src/Laravel/Constraint/Bus/WasHandledTest.php b/src/Laravel/Constraint/Bus/WasHandledTest.php index 372d60b..baf9715 100644 --- a/src/Laravel/Constraint/Bus/WasHandledTest.php +++ b/src/Laravel/Constraint/Bus/WasHandledTest.php @@ -4,6 +4,7 @@ namespace Craftzing\TestBench\Laravel\Constraint\Bus; +use Craftzing\TestBench\PHPUnit\Constraint\Callables\Assertions\WithSameArguments; use Craftzing\TestBench\PHPUnit\Constraint\Callables\WasCalled; use Craftzing\TestBench\PHPUnit\Constraint\Objects\DeriveConstraintsFromObjectUsingFakes; use Craftzing\TestBench\PHPUnit\Constraint\Objects\DeriveConstraintsFromObjectUsingReflection; @@ -313,18 +314,10 @@ public function itDerivesConstraintsFromExpectedCommandsAndMatchesItAgainstActua $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), - ); + $deriveConstraints->invoke->assert(new WasCalled(new WithSameArguments($expected))); + $deriveConstraints->invoke->assert(new WasCalled(new WithSameArguments($actual))->never()); + $constraint->matches->assert(new WasCalled(new WithSameArguments($actual))); + $constraint->matches->assert(new WasCalled(new WithSameArguments($expected))->never()); } private function command(array $properties): stdClass diff --git a/src/PHPUnit/Constraint/Callables/Assertions/WithEqualArguments.php b/src/PHPUnit/Constraint/Callables/Assertions/WithEqualArguments.php new file mode 100644 index 0000000..778882e --- /dev/null +++ b/src/PHPUnit/Constraint/Callables/Assertions/WithEqualArguments.php @@ -0,0 +1,40 @@ +expected = $expected; + } + + public function __invoke(mixed ...$actual): void + { + Assert::assertCount( + count($this->expected), + $actual, + 'Callable was invoked with a different amount of arguments.', + ); + + foreach ($actual as $key => $value) { + // @mago-expect analyzer:invalid-operand + $argumentNumber = $key + 1; + + Assert::assertEquals( + $this->expected[$key], + $value, + "Argument #{$argumentNumber} passed to callable does not match expected value.", + ); + } + } +} diff --git a/src/PHPUnit/Constraint/Callables/Assertions/WithEqualArgumentsTest.php b/src/PHPUnit/Constraint/Callables/Assertions/WithEqualArgumentsTest.php new file mode 100644 index 0000000..ffa5521 --- /dev/null +++ b/src/PHPUnit/Constraint/Callables/Assertions/WithEqualArgumentsTest.php @@ -0,0 +1,69 @@ +expectException(ExpectationFailedException::class); + $this->expectExceptionMessage('was invoked with a different amount of arguments'); + + $instance->__invoke(...$actual); + } + + #[Test] + #[TestWith([1, 'first different', 'last'])] + #[TestWith([2, 'first', 'last different'])] + public function itFailsWhenInvokedWithDifferentArguments(int $argument, string ...$actual): void + { + $instance = new WithEqualArguments('first', 'last'); + + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessage("Argument #{$argument} passed to callable does not match expected value"); + + $instance->__invoke(...$actual); + } + + #[Test] + public function itFailsWhenInvokedWithArgumentsInDifferentOrder(): void + { + $instance = new WithEqualArguments('first', 'last'); + + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessage('Argument #1 passed to callable does not match expected value'); + + $instance->__invoke('last', 'first'); + } + + #[Test] + #[TestWith(['first', 'second', 'last'], 'Scalar arguments')] + #[TestWith([new stdClass()], 'Object arguments')] + public function itPassesWhenInvokedWithSameArguments(mixed ...$actual): void + { + $instance = new WithEqualArguments(...$actual); + + $instance->__invoke(...$actual); + } + + #[Test] + public function itPassesWhenInvokedWithEqualArguments(): void + { + $instance = new WithEqualArguments(new stdClass()); + + $instance->__invoke(new stdClass()); + } +} diff --git a/src/PHPUnit/Constraint/Callables/Assertions/WithSameArguments.php b/src/PHPUnit/Constraint/Callables/Assertions/WithSameArguments.php new file mode 100644 index 0000000..3d9208b --- /dev/null +++ b/src/PHPUnit/Constraint/Callables/Assertions/WithSameArguments.php @@ -0,0 +1,40 @@ +expected = $expected; + } + + public function __invoke(mixed ...$actual): void + { + Assert::assertCount( + count($this->expected), + $actual, + 'Callable was invoked with a different amount of arguments.', + ); + + foreach ($actual as $key => $value) { + // @mago-expect analyzer:invalid-operand + $argumentNumber = $key + 1; + + Assert::assertSame( + $this->expected[$key], + $value, + "Argument #{$argumentNumber} passed to callable does not match expected value.", + ); + } + } +} diff --git a/src/PHPUnit/Constraint/Callables/Assertions/WithSameArgumentsTest.php b/src/PHPUnit/Constraint/Callables/Assertions/WithSameArgumentsTest.php new file mode 100644 index 0000000..ce7bb0a --- /dev/null +++ b/src/PHPUnit/Constraint/Callables/Assertions/WithSameArgumentsTest.php @@ -0,0 +1,72 @@ +expectException(ExpectationFailedException::class); + $this->expectExceptionMessage('was invoked with a different amount of arguments'); + + $instance->__invoke(...$actual); + } + + #[Test] + #[TestWith([1, 'first different', 'last'])] + #[TestWith([2, 'first', 'last different'])] + public function itFailsWhenInvokedWithDifferentArguments(int $argument, string ...$actual): void + { + $instance = new WithSameArguments('first', 'last'); + + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessage("Argument #{$argument} passed to callable does not match expected value"); + + $instance->__invoke(...$actual); + } + + #[Test] + public function itFailsWhenInvokedWithArgumentsInDifferentOrder(): void + { + $instance = new WithSameArguments('first', 'last'); + + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessage('Argument #1 passed to callable does not match expected value'); + + $instance->__invoke('last', 'first'); + } + + #[Test] + public function itFailsWhenInvokedWithNonIdenticalArguments(): void + { + $instance = new WithSameArguments(new stdClass()); + + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessage('Argument #1 passed to callable does not match expected value'); + + $instance->__invoke(new stdClass()); + } + + #[Test] + #[TestWith(['first', 'second', 'last'], 'Scalar arguments')] + #[TestWith([new stdClass()], 'Object arguments')] + public function itPassesWhenInvokedWithSameArguments(mixed ...$actual): void + { + $instance = new WithSameArguments(...$actual); + + $instance->__invoke(...$actual); + } +} diff --git a/src/PHPUnit/Constraint/Callables/WasCalled.php b/src/PHPUnit/Constraint/Callables/WasCalled.php index 37751de..3ddb02d 100644 --- a/src/PHPUnit/Constraint/Callables/WasCalled.php +++ b/src/PHPUnit/Constraint/Callables/WasCalled.php @@ -4,53 +4,47 @@ namespace Craftzing\TestBench\PHPUnit\Constraint\Callables; -use Closure; use Craftzing\TestBench\PHPUnit\Constraint\ProvidesAdditionalFailureDescription; use Craftzing\TestBench\PHPUnit\Constraint\Quantable; use Craftzing\TestBench\PHPUnit\Doubles\CallableInvocation; use Craftzing\TestBench\PHPUnit\Doubles\SpyCallable; use InvalidArgumentException; use Override; -use PHPUnit\Framework\Assert; use PHPUnit\Framework\Constraint\Constraint; use PHPUnit\Framework\ExpectationFailedException; use function array_filter; use function count; +use function is_callable; +use function spl_object_id; final class WasCalled extends Constraint implements Quantable { use ProvidesAdditionalFailureDescription; + /** @var callable|null */ + public readonly mixed $withArguments; + public function __construct( - public readonly ?Closure $assertInvocation = null, + ?callable $withArguments = null, public readonly ?int $times = null, - ) {} - - public function withSame(mixed ...$expected): self - { - return new self(static function (mixed ...$actual) use ($expected): void { - Assert::assertCount(count($expected), $actual); - - foreach ($actual as $key => $value) { - Assert::assertSame($expected[$key], $value); - } - }, $this->times); + ) { + $this->withArguments = $withArguments; } public function times(int $count): self { - return new self($this->assertInvocation, $count); + return new self($this->withArguments, $count); } public function never(): self { - return new self($this->assertInvocation, 0); + return new self($this->withArguments, 0); } public function once(): self { - return new self($this->assertInvocation, 1); + return new self($this->withArguments, 1); } #[Override] @@ -62,7 +56,7 @@ protected function matches(mixed $other): bool ); } - $matchingInvocations = array_filter($other->invocations, $this->matchesInvocationAssertions(...)); + $matchingInvocations = array_filter($other->invocations, $this->wasInvokedWithExpectedArguments(...)); return match ($this->times) { null => $matchingInvocations !== [], @@ -70,11 +64,14 @@ protected function matches(mixed $other): bool }; } - private function matchesInvocationAssertions(CallableInvocation $invocation): bool + private function wasInvokedWithExpectedArguments(CallableInvocation $invocation): bool { + if (!is_callable($this->withArguments)) { + return true; + } + try { - // @mago-expect analyzer:invalid-method-access - $this->assertInvocation?->__invoke(...$invocation->arguments); + ( $this->withArguments )(...$invocation->arguments); } catch (ExpectationFailedException $expectationFailed) { $this->additionalFailureDescriptions[] = $expectationFailed->getMessage(); @@ -84,6 +81,22 @@ private function matchesInvocationAssertions(CallableInvocation $invocation): bo return true; } + protected function failureDescription(mixed $other): string + { + if (!$other instanceof SpyCallable) { + return parent::failureDescription($other); + } + + $message = $other::class . '#' . spl_object_id($other); + + if ($this->times !== null) { + $totalInvocations = count($other->invocations); + $message .= " (with {$totalInvocations} total invocations)"; + } + + return "{$message} {$this->toString()}"; + } + public function toString(): string { $message = 'was called'; @@ -92,8 +105,8 @@ public function toString(): string $message .= " {$this->times} time(s)"; } - if ($this->assertInvocation !== null) { - $message .= ' with given invocation assertions'; + if ($this->withArguments !== null) { + $message .= ' with given arguments'; } return $message; diff --git a/src/PHPUnit/Constraint/Callables/WasCalledTest.php b/src/PHPUnit/Constraint/Callables/WasCalledTest.php index c31a1c9..bafb1d8 100644 --- a/src/PHPUnit/Constraint/Callables/WasCalledTest.php +++ b/src/PHPUnit/Constraint/Callables/WasCalledTest.php @@ -22,7 +22,7 @@ public function itCanConstructWithoutArguments(): void { $instance = new WasCalled(); - $this->assertNull($instance->assertInvocation); + $this->assertNull($instance->withArguments); $this->assertNull($instance->times); } @@ -33,7 +33,7 @@ public function itCanConstructWithInvocationAssertions(): void $instance = new WasCalled($assertInvocation); - $this->assertSame($assertInvocation, $instance->assertInvocation); + $this->assertSame($assertInvocation, $instance->withArguments); $this->assertNull($instance->times); } @@ -47,9 +47,9 @@ public function itImplementsTheQuantableInterface(QuantableConstraint $quantise) $quantisedInstance = $quantise($instance); $this->assertNull($instance->times); - $this->assertSame($assertInvocation, $instance->assertInvocation); + $this->assertSame($assertInvocation, $instance->withArguments); $this->assertSame($quantise->times, $quantisedInstance->times); - $this->assertSame($assertInvocation, $quantisedInstance->assertInvocation); + $this->assertSame($assertInvocation, $quantisedInstance->withArguments); } #[Test] @@ -87,7 +87,7 @@ public function itFailsWhenNotCalledWithExpectedArguments(): void $callable('last', 'first'); $this->expectException(ExpectationFailedException::class); - $this->expectExceptionMessage('was called with given invocation assertions.'); + $this->expectExceptionMessage('was called with given arguments.'); $this->assertThat($callable, new WasCalled(function (string $first, string $last): void { $this->assertSame('first', $first); @@ -140,7 +140,7 @@ public function itFailsWhenNotCalledExpectedTimesWithExpectedArguments(Quantable $quantise->applyTo(static fn() => $callable('first', 'last')); $this->expectException(ExpectationFailedException::class); - $this->expectExceptionMessage("was called {$quantise->expected} time(s) with given invocation assertions."); + $this->expectExceptionMessage("was called {$quantise->expected} time(s) with given arguments."); $this->assertThat( $callable, diff --git a/src/PHPUnit/DataProviders/QuantableConstraintTest.php b/src/PHPUnit/DataProviders/QuantableConstraintTest.php index 4b78a46..07b3437 100644 --- a/src/PHPUnit/DataProviders/QuantableConstraintTest.php +++ b/src/PHPUnit/DataProviders/QuantableConstraintTest.php @@ -4,6 +4,7 @@ namespace Craftzing\TestBench\PHPUnit\DataProviders; +use Craftzing\TestBench\PHPUnit\Constraint\Callables\Assertions\WithSameArguments; use Craftzing\TestBench\PHPUnit\Constraint\Callables\WasCalled; use Craftzing\TestBench\PHPUnit\Constraint\Quantable; use Craftzing\TestBench\PHPUnit\Doubles\SpyCallable; @@ -63,11 +64,7 @@ public function itCanBeInvokedOnConstraints(string $method, int $times): void $instance($constraint); - $constraint->spy->assert( - new WasCalled() - ->withSame($instance->method, $instance->times) - ->once(), - ); + $constraint->spy->assert(new WasCalled(new WithSameArguments($instance->method, $instance->times))->once()); } #[Test]