PHPUnit: Testing Closure passed to Collaborator
Yes! Closure is callable, so you can just call __invoke() to the closure returned when test it! This is happen when we, for example, have a class and function that have closure inside it like the following:
class Awesome
{
public function call($foo)
{
return function() use ($foo) {
return $foo;
};
}
}
This can be tested with:
use Awesome;
use PHPUnit_Framework_TestCase;
class AwesomeTest extends PHPUnit_Framework_TestCase
{
protected function setUp()
{
$this->awesome = new Awesome();
}
public function testCall()
{
$foo = 'foo';
$call = $this->awesome->call($foo);
$this->assertTrue(is_callable($call));
$invoked = $call->__invoke();
$this->assertEquals($foo, $invoked);
}
}
We need an __invoke() call, as the closure never executed before it invoked. So, we need to call that.
On Collaborator Case
The problem is when we have a collaborator, and closure is processed inside the collaborator:
class Awesome
{
private $awesomeDependency;
public function __construct(AwesomeDependency $awesomeDependency)
{
$this->awesomeDependency = $awesomeDependency;
}
public function call($foo)
{
$closure = function() use ($foo) {
return $foo;
};
return $this->awesomeDependency->call($closure);
}
}
and the closure inside call only executed in the AwesomeDependency class:
class AwesomeDependency
{
public function call($call)
{
return $call();
}
}
Our test can be like the following:
use Awesome;
use AwesomeDependency;
use PHPUnit_Framework_TestCase;
class AwesomeTest extends PHPUnit_Framework_TestCase
{
protected function setUp()
{
$this->awesomeDependency = $this->prophesize(AwesomeDependency::class);
$this->awesome = new Awesome($this->awesomeDependency->reveal());
}
public function testCall()
{
$foo = 'foo';
$closure = function() use ($foo) {
return $foo;
};
$this->awesomeDependency
->call($closure)
->will(function() use ($closure) {
return $closure->__invoke();
})
->shouldBeCalled();
$call = $this->awesome->call($foo);
}
}
As we can see, the $this->awesomeDependency is act as a mock, and calling __invoke() in will() is represent a $closure that already passed to the mock, not the original $closure one, and we will get partial coverage:

We know now, it won’t coverable as completed! What we can do? A refactor! But wait, it may be a legacy code, an aggressive refactor may cause problem, so a little but works patch may work for it.
- Make a
$closureas class property, and add mutator and accessor for it.
class Awesome
{
private $closure;
// ...
public function setClosure($closure)
{
$this->closure = $closure;
}
public function getClosure()
{
return $this->closure;
}
// ...
}
- Set
$closureproperty when callcall()function:
class Awesome
{
// ...
public function call($foo)
{
$this->setClosure(function() use ($foo) {
return $foo;
});
return $this->awesomeDependency->call($this->getClosure());
}
}
- And in tests, we can now has:
class AwesomeTest extends PHPUnit_Framework_TestCase
{
// ...
public function testCall()
{
$foo = 'foo';
$closure = function() use ($foo) {
return $foo;
};
$awesome = $this->awesome;
$this->awesomeDependency
->call($closure)
->will(function() use ($awesome) {
return $awesome->getClosure()->__invoke();
})
->shouldBeCalled();
$call = $this->awesome->call($foo);
$this->assertEquals($foo, $call);
}
}

Need a better way? We can replace a closure with an array callback, so, we add additional function that called via call_user_func_array():
class Awesome
{
public function call($foo)
{
return $this->awesomeDependency->call(call_user_func_array(
[$this, 'onFoo'],
[$foo]
));
}
public function onFoo($foo)
{
return function() use ($foo) {
return $foo;
};
}
}
And in our tests, we can do:
public function testCall()
{
$foo = 'foo';
$closure = function() use ($foo) {
return $foo;
};
$awesome = $this->awesome;
$this->awesomeDependency->call($closure)
->will(function() use ($awesome, $foo) {
return $awesome->onFoo($foo)->__invoke();
})
->shouldBeCalled();
$call = $this->awesome->call($foo);
$this->assertEquals($foo, $call);
}
And, we now have a fully coverage too:

leave a comment