使用闭包进行 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 单元测试的主要内容,如果未能解决你的问题,请参考以下文章
如何使用 Doctrine Fixture 加载测试数据库进行 PHP 单元测试?