PHPUnit 中的模拟 - 具有不同参数的同一方法的多个配置
Posted
技术标签:
【中文标题】PHPUnit 中的模拟 - 具有不同参数的同一方法的多个配置【英文标题】:Mock in PHPUnit - multiple configuration of the same method with different arguments 【发布时间】:2011-07-25 23:17:58 【问题描述】:可以这样配置phpUnit mock吗?
$context = $this->getMockBuilder('Context')
->getMock();
$context->expects($this->any())
->method('offsetGet')
->with('Matcher')
->will($this->returnValue(new Matcher()));
$context->expects($this->any())
->method('offsetGet')
->with('Logger')
->will($this->returnValue(new Logger()));
我使用 PHPUnit 3.5.10,当我请求 Matcher 时它失败了,因为它需要“Logger”参数。 就像第二个期望是重写第一个,但是当我转储模拟时,一切看起来都很好。
【问题讨论】:
【参考方案1】:遗憾的是,默认的 PHPUnit Mock API 无法做到这一点。
我可以看到两个选项可以让你接近这样的事情:
使用 ->at($x)
$context = $this->getMockBuilder('Context')
->getMock();
$context->expects($this->at(0))
->method('offsetGet')
->with('Matcher')
->will($this->returnValue(new Matcher()));
$context->expects($this->at(1))
->method('offsetGet')
->with('Logger')
->will($this->returnValue(new Logger()));
这可以正常工作,但您的测试超出了应有的范围(主要是它首先使用匹配器调用,这是一个实现细节)。
如果您对每个函数都有多次调用,这也会失败!
接受两个参数并使用returnCallBack
这是更多的工作,但效果更好,因为您不依赖于调用的顺序:
工作示例:
<?php
class FooTest extends PHPUnit_Framework_TestCase
public function testX()
$context = $this->getMockBuilder('Context')
->getMock();
$context->expects($this->exactly(2))
->method('offsetGet')
->with($this->logicalOr(
$this->equalTo('Matcher'),
$this->equalTo('Logger')
))
->will($this->returnCallback(
function($param)
var_dump(func_get_args());
// The first arg will be Matcher or Logger
// so something like "return new $param" should work here
));
$context->offsetGet("Matcher");
$context->offsetGet("Logger");
class Context
public function offsetGet() echo "org";
这将输出:
/*
$ phpunit footest.php
PHPUnit 3.5.11 by Sebastian Bergmann.
array(1)
[0]=>
string(7) "Matcher"
array(1)
[0]=>
string(6) "Logger"
.
Time: 0 seconds, Memory: 3.00Mb
OK (1 test, 1 assertion)
我在匹配器中使用了$this->exactly(2)
来表明这也适用于计算调用。如果您不需要将其换成$this->any()
,当然可以。
【讨论】:
很好的解决方案!我同意你的观点,调用顺序不应该泄漏到测试中。但是,使用第二种方法,您无法保证该方法是否使用每个单独的预期值调用一次。也就是说,在您的示例中,代码可以使用Matcher
作为参数调用上下文两次,并且永远不会使用Logger
作为参数,它仍然会通过。取决于可能是问题的测试。在不将顺序泄漏到测试用例中的情况下,您如何解决这个问题?
@MarijnHuizendveld “绑定一系列预期调用的闭包,删除它看到的每个人,并在测试结束时删除一个 assertEmpty”将是首先想到的。
请注意 ->at($x) 包括对其他方法的函数调用,所以如果你先模拟另一个方法,不管它是同一个方法还是使用 ->at($x)本身,$x 从 1 而不是 0 开始。
仍然 $this->returnCallback 是唯一的解决方案:/【参考方案2】:
从 PHPUnit 3.6 开始,$this->returnValueMap()
可用于根据给定的方法存根参数返回不同的值。
【讨论】:
是的,您可以使用 returnValueMap() 但请确保您指定了该方法所需的所有参数,否则它无法正常工作。见github.com/sebastianbergmann/phpunit-mock-objects/issues/89【参考方案3】:您可以通过回调实现此目的:
class MockTest extends PHPUnit_Framework_TestCase
/**
* @dataProvider provideExpectedInstance
*/
public function testMockReturnsInstance($expectedInstance)
$context = $this->getMock('Context');
$context->expects($this->any())
->method('offsetGet')
// Accept any of "Matcher" or "Logger" for first argument
->with($this->logicalOr(
$this->equalTo('Matcher'),
$this->equalTo('Logger')
))
// Return what was passed to offsetGet as a new instance
->will($this->returnCallback(
function($arg1)
return new $arg1;
));
$this->assertInstanceOf(
$expectedInstance,
$context->offsetGet($expectedInstance)
);
public function provideExpectedInstance()
return array_chunk(array('Matcher', 'Logger'), 1);
应将任何“Logger”或“Matcher”参数传递给 Context Mock 的 offsetGet
方法:
F:\Work\code\gordon\sandbox>phpunit NewFileTest.php
PHPUnit 3.5.13 by Sebastian Bergmann.
..
Time: 0 seconds, Memory: 3.25Mb
OK (2 tests, 4 assertions)
如您所见,PHPUnit 运行了两个测试。每个 dataProvider 值一个。在每一项测试中,它都为with()
和instanceOf
做出断言,因此有四个断言。
【讨论】:
【参考方案4】:根据@edorian 和 cmets (@MarijnHuizendveld) 的回答,关于确保使用 Matcher 和 Logger 调用该方法,而不是简单地使用 Matcher 或 Logger 调用两次,这里是一个示例。
$expectedArguments = array('Matcher', 'Logger');
$context->expects($this->exactly(2))
->method('offsetGet')
->with($this->logicalOr(
$this->equalTo('Matcher'),
$this->equalTo('Logger')
))
->will($this->returnCallback(
function($param) use (&$expectedArguments)
if(($key = array_search($param, $expectedArguments)) !== false)
// remove called argument from list
unset($expectedArguments[$key]);
// The first arg will be Matcher or Logger
// so something like "return new $param" should work here
));
// perform actions...
// check all arguments removed
$this->assertEquals(array(), $expectedArguments, 'Method offsetGet not called with all required arguments');
这适用于 PHPUnit 3.7。
如果您正在测试的方法实际上没有返回任何内容,而您只需要测试它是否使用正确的参数调用,则同样的方法也适用。对于这种情况,我还尝试使用 $this->callback 的回调函数作为 with 的参数,而不是遗嘱中的 returnCallback。这失败了,因为内部 phpunit 在验证参数匹配器回调的过程中调用了回调两次。这意味着该方法在第二次调用时失败,该参数已从预期的参数数组中删除。我不知道为什么 phpunit 会调用它两次(似乎是不必要的浪费),我想您可以通过仅在第二次调用时将其删除来解决此问题,但我没有足够的信心认为这是预期的和一致的 phpunit 行为依靠发生的事情。
【讨论】:
非常感谢您花时间打出一个例子!非常有帮助。 :)【参考方案5】:我的 2 美分话题:使用 at($x) 时要注意:这意味着预期的方法调用将是模拟对象上的第 ($x+1) 个方法调用;这并不意味着这将是预期方法的第 ($x+1) 次调用。这让我浪费了一些时间,所以我希望它不会和你在一起。向大家致以亲切的问候。
【讨论】:
【参考方案6】:我偶然发现了这个用于模拟对象的 PHP 扩展:https://github.com/etsy/phpunit-extensions/wiki/Mock-Object
【讨论】:
【参考方案7】:这里还有一些doublit 库的解决方案:
解决方案 1:使用Stubs::returnValueMap
/* Get a dummy double instance */
$double = Doublit::dummy_instance(Context::class);
/* Test the "offsetGet" method */
$double::_method('offsetGet')
// Test that the first argument is equal to "Matcher" or "Logger"
->args([Constraints::logicalOr('Matcher', 'Logger')])
// Return "new Matcher()" when first argument is "Matcher"
// Return "new Logger()" when first argument is "Logger"
->stub(Stubs::returnValueMap([['Matcher'], ['Logger']], [new Matcher(), new Logger()]));
解决方案 2:使用回调
/* Get a dummy double instance */
$double = Doublit::dummy_instance(Context::class);
/* Test the "offsetGet" method */
$double::_method('offsetGet')
// Test that the first argument is equal to "Matcher" or "Logger"
->args([Constraints::logicalOr('Matcher', 'Logger')])
// Return "new Matcher()" when first argument $arg is "Matcher"
// Return "new Logger()" when first argument $arg is "Logger"
->stub(function($arg)
if($arg == 'Matcher')
return new Matcher();
else if($arg == 'Logger')
return new Logger();
);
【讨论】:
以上是关于PHPUnit 中的模拟 - 具有不同参数的同一方法的多个配置的主要内容,如果未能解决你的问题,请参考以下文章