Skip to content
Merged
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
17 changes: 5 additions & 12 deletions src/Laravel/Constraint/Bus/WasHandledTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
40 changes: 40 additions & 0 deletions src/PHPUnit/Constraint/Callables/Assertions/WithEqualArguments.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace Craftzing\TestBench\PHPUnit\Constraint\Callables\Assertions;

use PHPUnit\Framework\Assert;

use function count;

final readonly class WithEqualArguments
{
/** @var array */
private array $expected;

public function __construct(mixed ...$expected)
{
$this->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.",
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

declare(strict_types=1);

namespace Craftzing\TestBench\PHPUnit\Constraint\Callables\Assertions;

use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\TestWith;
use PHPUnit\Framework\ExpectationFailedException;
use PHPUnit\Framework\TestCase;
use stdClass;

final class WithEqualArgumentsTest extends TestCase
{
#[Test]
#[TestWith(['first'], 'Too few arguments')]
#[TestWith(['first', 'second', 'last'], 'Too many arguments')]
public function itFailsWhenInvokedWithDifferentAmountOfArguments(string ...$actual): void
{
$instance = new WithEqualArguments('first', 'last');

$this->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());
}
}
40 changes: 40 additions & 0 deletions src/PHPUnit/Constraint/Callables/Assertions/WithSameArguments.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace Craftzing\TestBench\PHPUnit\Constraint\Callables\Assertions;

use PHPUnit\Framework\Assert;

use function count;

final readonly class WithSameArguments
{
/** @var array */
private array $expected;

public function __construct(mixed ...$expected)
{
$this->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.",
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

declare(strict_types=1);

namespace Craftzing\TestBench\PHPUnit\Constraint\Callables\Assertions;

use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\Attributes\TestWith;
use PHPUnit\Framework\ExpectationFailedException;
use PHPUnit\Framework\TestCase;
use stdClass;

final class WithSameArgumentsTest extends TestCase
{
#[Test]
#[TestWith(['first'], 'Too few arguments')]
#[TestWith(['first', 'second', 'last'], 'Too many arguments')]
public function itFailsWhenInvokedWithDifferentAmountOfArguments(string ...$actual): void
{
$instance = new WithSameArguments('first', 'last');

$this->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);
}
}
59 changes: 36 additions & 23 deletions src/PHPUnit/Constraint/Callables/WasCalled.php
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -62,19 +56,22 @@ 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 !== [],
default => count($matchingInvocations) === $this->times,
};
}

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();

Expand All @@ -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';
Expand All @@ -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;
Expand Down
Loading