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
$closure
as 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
$closure
property 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