Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PHPUnit testing with closures

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?

like image 284
Schleis Avatar asked Oct 08 '13 18:10

Schleis


People also ask

What is a PHPUnit test?

PHPUnit is a unit testing framework for the PHP programming language. It is an instance of the xUnit design for unit testing systems that began with SUnit and became popular with JUnit. Even a small software development project usually takes hours of hard work.

How do I run a PHPUnit test?

How to Run Tests in PHPUnit. You can run all the tests in a directory using the PHPUnit binary installed in your vendor folder. You can also run a single test by providing the path to the test file. You use the --verbose flag to get more information on the test status.

What is PHPUnit used for?

Introduction. PHPUnit is one of the oldest and most well-known unit testing packages for PHP. It is primarily designed for unit testing, which means testing your code in the smallest components possible, but it is also incredibly flexible and can be used for a lot more than just unit testing.


2 Answers

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);
like image 150
Sergey Kolodyazhnyy Avatar answered Sep 30 '22 12:09

Sergey Kolodyazhnyy


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);
    }
}
like image 9
FtDRbwLXw6 Avatar answered Sep 30 '22 10:09

FtDRbwLXw6