7

For learning purposes I was trying to write a unit test for one of my Magento 2 around plugins (interception).

The method I'm testing receives as parameter a \Closure object that cannot be mocked because \Closure is a final class

How do I proceed in this case? Create an actual closure in my test class and pass that as parameter? Or any other way?

Let's say my method looks like this. Ignore the content, focus on the concept.

class Something
{
    public function aroundDoStuff(SomeModel $subject \Closure $closure, $extraParam)
    {
        if ($subject->getSomeValue() == 1) {
             return 'Custom return';
        }
        return $closure($extraParam);
    }
}

Now for my test, I mocked the first parameter SomeModel.

$subject = $this->getMock(SomeModel::class, [], [], '', false);
$subject->expects($this->any())->method('getSomeValue')->will($this->returnValue('1'));

$closure = ????; // WHAT GOES HERE
$obj = new Something();
$expected = 'Custom return';
$extraParam = 'not important';
$this->assertEquals($expected, $obj->aroundDoStuff($subject, $closure, $extraParam));
1
  • This class in the core uses a closureMock. You may be able to do something similar.
    – user50408
    Mar 2, 2017 at 21:07

2 Answers 2

5

To mock callables, I usually mock __invoke:

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

$callbackMock->expects($this->once())->method('__invoke'); // etc.

The problem is that Magento uses \Closure type hints instead of callable. As soon as Magento supports PHP 7.1 you will be able to use Closure::fromCallable($callbackMock), until then, wrap it yourself:

$closure = function(...$args) use ($callbackMock) {
    return $callbackMock(...$args);
};

That aside, I would not bother writing unit tests for plugins most of the time. If there is business logic that I write with unit tests, this would be in another class where the plugin delegates to. To test if the plugin works correctly, an integration test is more appropiate.

2
  • This looks nice. Thanks for it I will test tomorrow at the office and come back with the results. As for your side not, not bothering to write unit tests for plugins, I don't know.... Usually plugins have a simple logic. And if it takes 5 minutes to write the test why not do it? What's the harm in that?
    – Marius
    Mar 2, 2017 at 22:19
  • No harm, and it sure is a case by case decision. But if it's not a five minute test, its value is probably less than its cost. I prefer to not write unit tests for integration points of the framework like plugins, controllers or observers, because the setup tends to be complicated and full of assumptions I have no control over. But this does not mean that you don't test the logic, but that you should keep these integration points as stupid as possible and test the logic separately. Mar 2, 2017 at 23:16
0

Whereas the upper solution works, it's deprecated by PHPUnit.

Instead, you can do:

$this->getMockBuilder(\stdClass::class)->setMethods(['__invoke'])->getMock();
2
  • thanks. but this is exactly what the accepted answers says.
    – Marius
    Mar 18, 2020 at 14:04
  • Wow, my bad I thought I've read createPartialMock instead (which is deprecated in this case)
    – mtarld
    Mar 19, 2020 at 15:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.