使用闭包进行 PHP 单元测试

Posted

技术标签:

【中文标题】使用闭包进行 PHP 单元测试【英文标题】:PHPUnit testing with closures 【发布时间】:2013-10-15 19:36:20 【问题描述】:

试图为类的方法编写一个测试,该方法调用带有闭包的模拟方法。你将如何验证被调用的闭包?

我知道您可以断言该参数是Closure 的一个实例。但是您将如何检查有关关闭的任何信息?

例如,您将如何验证传递的函数:

 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);
     
 

您无法比较 PHP 中的两个闭包。 PHPUnit中有没有办法执行传入的参数或以某种方式验证它?

【问题讨论】:

【参考方案1】:

如果你想模拟一个匿名函数(回调),你可以用__invoke 方法模拟一个类。例如:

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

$someServiceYouAreTesting->testedMethod($shouldBeCalled);

如果您使用的是最新的 PHPUnit,则必须使用 mock builder 来解决问题:

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

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

$someServiceYouAreTesting->testedMethod($shouldBeCalled);

您还可以为方法参数设置期望值或设置返回值,就像对任何其他方法一样:

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

【讨论】:

很好的答案!我想知道如何做到这一点已经有一段时间了。 这不仅有效,而且is_callable($shouldBeCalled) 也会返回true 聪明!但是,它不适用于带有引用的回调。【参考方案2】:

如果闭包在SUT 内部有一些副作用,可以在模拟调用后通过测试进行验证,请使用returnCallback 提供另一个闭包以使用传递的参数调用并将其返回值返回给@987654323 @。这将允许您调用SUT 的闭包来引起副作用。

 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));
     
 

【讨论】:

【参考方案3】:

您的问题是您没有注入依赖项(闭包),这总是使单元测试更加困难,并且可能使隔离变得不可能。

将闭包注入SUT::foo(),而不是在其中创建它,您会发现测试更加容易。

这是我将如何设计该方法(请记住,我对您的真实代码一无所知,因此这对您来说可能实用,也可能不实用):

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);
    

【讨论】:

好的,所以你建议我们做类似$someFunction = function() echo "I am an anonymous function"; ; $sut = new SUT(); $sut->foo($bar, $someFunction); 的事情。我们如何在那里测试它?我的观点是,匿名函数必须在somewhere 中定义。您可以根据需要将其传递到调用堆栈,但最终必须对其进行定义。那么如何测试呢? @Travesty3:我的答案包含重写的测试,向您展示了如何测试它。你测试它就像你测试其他任何使用注入依赖的东西一样——你模拟依赖并注入模拟。 @Travesty3 你可以有一个返回闭包的工厂类,然后你就可以单独测试闭包的逻辑 @drrcknlsn:您正在展示如何使用模拟的匿名函数测试 SUT::foo() 方法,而不是如何测试匿名函数。我的意思是,你如何验证“我是一个匿名函数”得到回应? @Travesty3:SUTTest::testFoo() 的工作是验证SUT::foo() 的行为是否符合预期。根据您粘贴的代码,SUT::foo() 唯一的工作就是调用该函数。我的答案中的测试验证了该函数是否被调用,因此它验证了 SUT::foo() 的行为符合预期。如果您需要测试函数本身,那么您可以在另一个专用的测试套件中执行此操作。为其提供已知的好/坏输入并验证输出(或相关的副作用)。

以上是关于使用闭包进行 PHP 单元测试的主要内容,如果未能解决你的问题,请参考以下文章

PHP单元测试框架PHPUnit的使用

如何使用 Doctrine Fixture 加载测试数据库进行 PHP 单元测试?

如何对 PHP 特征进行单元测试

使用无脂肪框架进行单元测试

PHP 单元测试 - 使用 Mockery 模拟静态自动加载类

如何使用php单元测试多组数据?