For our applications we use our custom constraint with specific map.
Previously we try to use callback (with call assertEquals
inside), but callback must return only boolean and if we try to check objects, the message was be simple - objects are not equals
, without diff and without any information about problem.
As result, we create our constaint:
<?php
declare(strict_types = 1);
namespace Acme\Tests\PhpUnit\Framework\Constraint;
use PHPUnit\Framework\Constraint\Constraint;
use PHPUnit\Framework\MockObject\Rule\InvocationOrder;
use SebastianBergmann\Comparator\ComparisonFailure;
use SebastianBergmann\Comparator\Factory;
class ConsecutiveMatches extends Constraint
{
/**
* @var ComparisonFailure|null
*/
private ?ComparisonFailure $failure = null;
/**
* Constructor.
*
* @param InvocationOrder $invocation
* @param array<int, mixed> $map
* @param bool $strict
*/
public function __construct(
private readonly InvocationOrder $invocation,
private readonly array $map,
private readonly bool $strict = true,
) {
}
/**
* {@inheritdoc}
*/
protected function matches(mixed $other): bool
{
$invokedCount = $this->invocation->numberOfInvocations();
if (\array_key_exists($invokedCount - 1, $this->map)) {
$expectedParam = $this->map[$invokedCount - 1];
} else if ($this->strict) {
throw new \InvalidArgumentException(\sprintf(
'Missed argument for matches (%d times).',
$invokedCount
));
}
$comparator = Factory::getInstance()->getComparatorFor($expectedParam, $other);
try {
$comparator->assertEquals($expectedParam, $other);
} catch (ComparisonFailure $error) {
$this->failure = $error;
return false;
}
return true;
}
/**
* {@inheritdoc}
*/
protected function failureDescription(mixed $other): string
{
return $this->failure ? $this->failure->getDiff() : parent::failureDescription($other);
}
/**
* {@inheritdoc}
*/
public function toString(): string
{
return '';
}
}
In this constraint we get the argument from map by invocation number.
And it very easy to usage:
#[Test]
public function shouldFoo(): void
{
$mock = $this->createMock(MyClass::class);
$matcher = new InvokedCount(2); // Should call 2 times
$mock->expects($matcher)
->method('someMethod')
->with(new ConsecutiveMatches($matcher, [$expectedArgumentForFirstCall, $expectedArgumentForSecondCall]))
->willReturnCallback(function () {
// You logic for return value.
// You can use custom map too for returns.
});
}
As result, we can use our constraint in more places.