Setting expectations for a closure using Mockery
If you are trying to set an expectation for \Closure
using mockery, you may have seen this error:
Mockery\Exception\NoMatchingExpectationException: No matching handler found for Mockery_0_NAMESPACE_CLASSNAME::METHODNAME(object(Closure)).
Either the method was unexpected or its arguments matched no expected argument list for this method
To start off with, anonymous functions in PHP are instances of the internal Closure class.
You probably tried something like passing a Closure to the expectation handler that was identical to the one being passed in the code.
This will not work since no two Closures have the same object hash.
To get around this, all you have to do is to use Mocker::on()
and then invoke the passed closure to see if the return value is what you were expecting. See below for a demonstration.
You can see a demonstration github repo here.
Here's some closure to this article with a little reading:
- http://www.ibm.com/developerworks/library/os-php-5.3new2/
- http://fabien.potencier.org/article/17/on-php-5-3-lambda-functions-and-closures
- http://php.net/manual/en/class.closure.php
- http://php.net/manual/en/functions.anonymous.php
Under Test
Here are the classes that we will be testing:
namespace Example;
use Closure;
class A {
public function foo(Closure $closure)
{
return $closure();
}
}
class B {
/**
* @var A
*/
private $a;
public function __construct(A $a)
{
$this->a = $a;
}
/**
* @param int $number
*/
public function bar($number)
{
return $this->a->foo(
function () use ($number) {
return $number * 5;
}
);
}
}
A test for the error
/**
* @test
*/
public function you_cannot_set_an_expectation_for_a_closure_by_passing_one()
{
// We know this will not work. Mockery is going to throw an exception.
$this->setExpectedException('Mockery\Exception\NoMatchingExpectationException');
$a = m::mock('Example\A');
$number = 5;
// This is the method that is expecting a \Closure object to be passed.
// It will raise an exception because, even though it is pretty much identical to the one that we use,
// it is still a distinct \Closure instance.
$a->shouldReceive('foo')->with(
function () use ($number) {
return $number * 5;
}
)->andReturn(25);
$b = new B($a);
// ::bar() is calling foo()
$this->assertSame(25, $b->bar($number));
}
Anonymous Functions and closure objects
/**
* http://php.net/manual/en/class.closure.php
* http://php.net/manual/en/functions.anonymous.php
* @test
*/
public function anonymous_functions_are_closure_objects()
{
$anonymousFunction = function () {
echo 'I am a \\Closure';
};
$this->assertSame("Closure", get_class($anonymousFunction));
}
No two closures are alike
/**
* @test
*/
public function closures_created_identically_will_never_have_the_same_object_hash()
{
$y = function ($number) {
return 5 * $number;
};
$z = function ($number) {
return 5 * $number;
};
// the only way to get these to match (that I know of) is to use a singleton
$this->assertNotSame(
spl_object_hash($y),
spl_object_hash($z)
);
}
But we can assert the return value of a Closure
/**
* @test
*/
public function it_can_expect_a_bool_from_a_closure_and_we_can_assert_the_desired_return_value()
{
$a = m::mock('Example\A');
// this is the method that is expecting a \Closure class to be passed
$a->shouldReceive('foo')->with(
m::on(
function ($closure) {
// this is so that we can know if the returned value from the \Closure is what we want.
$this->assertSame(25, $closure());
// you must return a bool here so that Mockery knows that the expectation passed
return is_callable($closure);
}
)
)->andReturn(25);
$b = new B($a);
// have bar() call foo()
$b->bar(5);
}