PHPUnit testing with closures

2019-02-06 05:15发布

问题:

This came up trying to write a test for a method of a class that calls a mock method with a closure. How would you verify the closure being called?

I know that you would be able to assert that the parameter is an instance of Closure. But how would you check anything about the closure?

For example how would you verify the function that is passed:

 class SUT {
     public function foo($bar) {
         $someFunction = function() { echo "I am an anonymous function"; };
         $bar->baz($someFunction);
     }
 }

 class SUTTest extends PHPUnit_Framework_TestCase {
     public function testFoo() {
         $mockBar = $this->getMockBuilder('Bar')
              ->setMethods(array('baz'))
              ->getMock();
         $mockBar->expects($this->once())
              ->method('baz')
              ->with( /** WHAT WOULD I ASSERT HERE? **/);

         $sut = new SUT();

         $sut->foo($mockBar);
     }
 }

You can't compare two closures in PHP. Is there a way in PHPUnit to execute the parameter passed in or in some way verify it?

回答1:

Your problem is that you aren't injecting your dependency (the closure), which always makes unit testing harder, and can make isolation impossible.

Inject the closure into SUT::foo() instead of creating it inside there and you'll find testing much easier.

Here is how I would design the method (bearing in mind that I know nothing about your real code, so this may or may not be practical for you):

class SUT 
{
    public function foo($bar, $someFunction) 
    {
        $bar->baz($someFunction);
    }
}

class SUTTest extends PHPUnit_Framework_TestCase 
{
    public function testFoo() 
    {
        $someFunction = function() {};

        $mockBar = $this->getMockBuilder('Bar')
             ->setMethods(array('baz'))
             ->getMock();
        $mockBar->expects($this->once())
             ->method('baz')
             ->with($someFunction);

        $sut = new SUT();

        $sut->foo($mockBar, $someFunction);
    }
}


回答2:

If you want to mock an anonymous function (callback) you can mock a class with __invoke method. For example:

$shouldBeCalled = $this->getMock(\stdClass::class, ['__invoke']);
$shouldBeCalled->expects($this->once())
    ->method('__invoke');

$someServiceYouAreTesting->testedMethod($shouldBeCalled);

If you are using latest PHPUnit, you would have to use mock builder to do the trick:

$shouldBeCalled = $this->getMockBuilder(\stdClass::class)
    ->setMethods(['__invoke'])
    ->getMock();

$shouldBeCalled->expects($this->once())
    ->method('__invoke');

$someServiceYouAreTesting->testedMethod($shouldBeCalled);

You can also set expectations for method arguments or set a returning value, just the same way you would do it for any other method:

$shouldBeCalled->expects($this->once())
    ->method('__invoke')
    ->with($this->equalTo(5))
    ->willReturn(15);


回答3:

If the closure has some side effects inside SUT that could be verified by the test after the mock invocation, use returnCallback to provide another closure to be called with the passed arguments and have its return value returned to SUT. This will allow you to call SUT's closure to cause the side effects.

 class SUT {
     public function foo($bar) {
         $someFunction = function() { return 5 * 3; };
         return $bar->baz($someFunction);
     }
 }

 class SUTTest extends PHPUnit_Framework_TestCase {
     public function testFoo() {
         $mockBar = $this->getMockBuilder('Bar')
              ->setMethods(array('baz'))
              ->getMock();
         $mockBar->expects($this->once())
              ->method('baz')
              ->will($this->returnCallback(function ($someFunction) {
                  return $someFunction();
              }));

         $sut = new SUT();

         self::assertEquals(15, $sut->foo($mockBar));
     }
 }