Is extending Mockery for commonly used assertions possible?
Asserting that a mocked function receives an instance of a class AND matching an identifier is cumbersome.
Mockery::on(
fn ($arg) => $arg instanceof App\Models\User && $arg->id === $user->id
)
Simplify this call by extending Mockery (via a Macro?) so that this assertion could be Mockery::onUser($user)
or Mockery::onUserOfId($id)
?
Pass in multiple arguments like Mockery::onUserOfRole($id, $user->role)
?
Note: the $user
object instance provided to the Mockery::on
call in the Test is not the same instance as is passed to the mocked function being tested, hence checking the type and matching the identifier.
A possible solution we have explored is extending the base TestCase
class with a matchModel()
function, but this extension is not "elegant". I have considered breaking it out into a Trait, but attaching these functions to all Test Cases, $this->onUserOfRole()
seems wrong. The solution also fails or needs to be customized. (Illuminate\Database\Eloquent\Model@is()
works, but...) if the Object is not a Laravel Model, then we're back to square one:
# Possible Solution that is not extensible and only works on Models
class TestCase extends Illuminate\Foundation\Testing\TestCase
protected function matchModel(Model $model)
{
return new MockeryClosure(function ($arg) use ($model) {
if ($model->is($arg)) {
return true;
}
// print custom error message
return false;
});
}
}
The Mockery::on method is just a one-line wrapper around the constructor for the \Mockery\Matcher\Closure
class, which in turn defines only two pieces of custom logic:
match
method which takes the parameter being examined, and returns true or false__toString
method returning the fixed string '<Closure===true>'
, presumably for use in error/debug messagesThe actual closure isn't defined directly as a property, it just relies on the constructor inherited from MatcherAbstract
which sets an untyped property $_expected
, but performs no other logic directly.
So a replacement for your use case can simply over-ride that constructor with whatever arguments you want, and use them in the match
method to implement your test, e.g.
class MyMatcher extends Mockery\Matcher\MatcherAbstract
{
private string $expectedClass;
private string $keyName;
private mixed $keyValue;
public function __construct(string $expectedClass, string $keyName, mixed $keyValue)
{
$this->expectedClass = $expectedClass;
$this->keyName = $keyName;
$this->keyValue = $keyValue;
}
/**
* Check if the actual value matches the expected.
*
* @param mixed $actual
* @return bool
*/
public function match(&$actual)
{
return
is_a($actual, $this->expectedClass, true)
&&
$actual->{$this->keyProperty} === $this->keyValue;
}
/**
* Return a string representation of this Matcher
*
* @return string
*/
public function __toString()
{
return "<Is {$this->expectedClass} and has {$this->keyProperty} of {$this->keyValue}>";
}
}
Then this:
Mockery::on(
fn ($arg) => $arg instanceof App\Models\User && $arg->id === $user->id
)
Becomes this:
new MyMatcher(App\Models\User::class, 'id', $user->id)
If you want to shorten the new MyMatcher
further, you can put a function wherever you like that wraps it in the same Mockery::on
wraps new \Mockery\Matcher\Closure
.
If you want more complex or configurable tests, all you need to figure out is a way to return a boolean result from match
.