php phpunit模拟方法多次调用不同的参数
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/5988616/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
phpunit mock method multiple calls with different arguments
提问by Aleksei Kornushkin
Is there any way to define different mock-expects for different input arguments? For example, I have database layer class called DB. This class has method called "Query ( string $query )", that method takes an SQL query string on input. Can I create mock for this class (DB) and set different return values for different Query method calls that depends on input query string?
有没有办法为不同的输入参数定义不同的模拟期望?例如,我有一个名为 DB 的数据库层类。此类具有名为“Query (string $query)”的方法,该方法在输入时采用 SQL 查询字符串。我可以为此类 (DB) 创建模拟并为取决于输入查询字符串的不同 Query 方法调用设置不同的返回值吗?
回答by hirowatari
It's not ideal to use at()
if you can avoid it because as their docs claim
at()
如果您可以避免使用它并不理想,因为正如他们的文档声称的那样
The $index parameter for the at() matcher refers to the index, starting at zero, in all method invocations for a given mock object. Exercise caution when using this matcher as it can lead to brittle tests which are too closely tied to specific implementation details.
at() 匹配器的 $index 参数指的是在给定模拟对象的所有方法调用中从零开始的索引。使用此匹配器时要小心,因为它可能导致与特定实现细节过于紧密相关的脆弱测试。
Since 4.1 you can use withConsecutive
eg.
从 4.1 开始,您可以使用withConsecutive
例如。
$mock->expects($this->exactly(2))
->method('set')
->withConsecutive(
[$this->equalTo('foo'), $this->greaterThan(0)],
[$this->equalTo('bar'), $this->greaterThan(0)]
);
If you want to make it return on consecutive calls:
如果你想让它在连续调用时返回:
$mock->method('set')
->withConsecutive([$argA1, $argA2], [$argB1], [$argC1, $argC2])
->willReturnOnConsecutiveCalls($retValueA, $retValueB, $retValueC);
回答by edorian
The PHPUnit Mocking library (by default) determines whether an expectation matches based solely on the matcher passed to expects
parameter and the constraint passed to method
. Because of this, two expect
calls that only differ in the arguments passed to with
will fail because both will match but only one will verify as having the expected behavior. See the reproduction case after the actual working example.
PHPUnit Mocking 库(默认情况下)仅根据传递给expects
参数的匹配器和传递给的约束来确定期望是否匹配method
。因此,两个expect
仅在传递给的参数方面不同的调用with
将失败,因为两者都会匹配,但只有一个会验证为具有预期的行为。请参阅实际工作示例后的复制案例。
For you problem you need to use ->at()
or ->will($this->returnCallback(
as outlined in another question on the subject
.
对于您需要使用的问题->at()
或->will($this->returnCallback(
如another question on the subject
.
Example:
例子:
<?php
class DB {
public function Query($sSql) {
return "";
}
}
class fooTest extends PHPUnit_Framework_TestCase {
public function testMock() {
$mock = $this->getMock('DB', array('Query'));
$mock
->expects($this->exactly(2))
->method('Query')
->with($this->logicalOr(
$this->equalTo('select * from roles'),
$this->equalTo('select * from users')
))
->will($this->returnCallback(array($this, 'myCallback')));
var_dump($mock->Query("select * from users"));
var_dump($mock->Query("select * from roles"));
}
public function myCallback($foo) {
return "Called back: $foo";
}
}
Reproduces:
再现:
phpunit foo.php
PHPUnit 3.5.13 by Sebastian Bergmann.
string(32) "Called back: select * from users"
string(32) "Called back: select * from roles"
.
Time: 0 seconds, Memory: 4.25Mb
OK (1 test, 1 assertion)
Reproduce why two ->with() calls dont' work:
重现为什么两个 ->with() 调用不起作用:
<?php
class DB {
public function Query($sSql) {
return "";
}
}
class fooTest extends PHPUnit_Framework_TestCase {
public function testMock() {
$mock = $this->getMock('DB', array('Query'));
$mock
->expects($this->once())
->method('Query')
->with($this->equalTo('select * from users'))
->will($this->returnValue(array('fred', 'wilma', 'barney')));
$mock
->expects($this->once())
->method('Query')
->with($this->equalTo('select * from roles'))
->will($this->returnValue(array('admin', 'user')));
var_dump($mock->Query("select * from users"));
var_dump($mock->Query("select * from roles"));
}
}
Results in
结果是
phpunit foo.php
PHPUnit 3.5.13 by Sebastian Bergmann.
F
Time: 0 seconds, Memory: 4.25Mb
There was 1 failure:
1) fooTest::testMock
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-select * from roles
+select * from users
/home/.../foo.php:27
FAILURES!
Tests: 1, Assertions: 0, Failures: 1
回答by Radu Murzea
From what I've found, the best way to solve this problem is by using PHPUnit's value-map functionality.
根据我的发现,解决这个问题的最好方法是使用 PHPUnit 的值映射功能。
Example from PHPUnit's documentation:
PHPUnit 文档中的示例:
class SomeClass {
public function doSomething() {}
}
class StubTest extends \PHPUnit_Framework_TestCase {
public function testReturnValueMapStub() {
$mock = $this->getMock('SomeClass');
// Create a map of arguments to return values.
$map = array(
array('a', 'b', 'd'),
array('e', 'f', 'h')
);
// Configure the mock.
$mock->expects($this->any())
->method('doSomething')
->will($this->returnValueMap($map));
// $mock->doSomething() returns different values depending on
// the provided arguments.
$this->assertEquals('d', $stub->doSomething('a', 'b'));
$this->assertEquals('h', $stub->doSomething('e', 'f'));
}
}
This test passes. As you can see:
此测试通过。如你看到的:
- when the function is called with parameters "a" and "b", "d" is returned
- when the function is called with parameters "e" and "f", "h" is returned
- 当使用参数“a”和“b”调用函数时,返回“d”
- 当使用参数“e”和“f”调用函数时,返回“h”
From what I can tell, this feature was introduced in PHPUnit 3.6, so it's "old" enough that it can be safely used on pretty much any development or staging environments and with any continuous integration tool.
据我所知,这个特性是在PHPUnit 3.6中引入的,所以它已经足够“老”了,可以安全地用于几乎任何开发或登台环境以及任何持续集成工具。
回答by joerx
It seems Mockery (https://github.com/padraic/mockery) supports this. In my case I want to check that 2 indices are created on a database:
似乎 Mockery ( https://github.com/padraic/mockery) 支持这一点。就我而言,我想检查是否在数据库上创建了 2 个索引:
Mockery, works:
嘲弄,作品:
use Mockery as m;
//...
$coll = m::mock(MongoCollection::class);
$db = m::mock(MongoDB::class);
$db->shouldReceive('selectCollection')->withAnyArgs()->times(1)->andReturn($coll);
$coll->shouldReceive('createIndex')->times(1)->with(['foo' => true]);
$coll->shouldReceive('createIndex')->times(1)->with(['bar' => true], ['unique' => true]);
new MyCollection($db);
PHPUnit, this fails:
PHPUnit,这失败了:
$coll = $this->getMockBuilder(MongoCollection::class)->disableOriginalConstructor()->getMock();
$db = $this->getMockBuilder(MongoDB::class)->disableOriginalConstructor()->getMock();
$db->expects($this->once())->method('selectCollection')->with($this->anything())->willReturn($coll);
$coll->expects($this->atLeastOnce())->method('createIndex')->with(['foo' => true]);
$coll->expects($this->atLeastOnce())->method('createIndex')->with(['bar' => true], ['unique' => true]);
new MyCollection($db);
Mockery also has a nicer syntax IMHO. It appears to be a tad slower than PHPUnits built-in mocking capability, but YMMV.
恕我直言,嘲笑也有更好的语法。它似乎比 PHPUnits 内置模拟功能慢一点,但是 YMMV。
回答by Lukas Lukac
Intro
介绍
Okay I see there is one solution provided for Mockery, so as I don't like Mockery, I am going to give you a Prophecy alternative but I would suggest you first to read about the difference between Mockery and Prophecy first.
好的,我看到为 Mockery 提供了一个解决方案,所以我不喜欢 Mockery,我会给你一个 Prophecy 替代方案,但我建议你首先阅读 Mockery 和 Prophecy 之间的区别。
Long story short: "Prophecy uses approach called message binding- it means that behaviour of the method does not change over time, but rather is changed by the other method."
长话短说:“预言使用称为消息绑定的方法- 这意味着该方法的行为不会随着时间的推移而改变,而是被另一种方法改变。”
Real world problematic code to cover
现实世界有问题的代码要覆盖
class Processor
{
/**
* @var MutatorResolver
*/
private $mutatorResolver;
/**
* @var ChunksStorage
*/
private $chunksStorage;
/**
* @param MutatorResolver $mutatorResolver
* @param ChunksStorage $chunksStorage
*/
public function __construct(MutatorResolver $mutatorResolver, ChunksStorage $chunksStorage)
{
$this->mutatorResolver = $mutatorResolver;
$this->chunksStorage = $chunksStorage;
}
/**
* @param Chunk $chunk
*
* @return bool
*/
public function process(Chunk $chunk): bool
{
$mutator = $this->mutatorResolver->resolve($chunk);
try {
$chunk->processingInProgress();
$this->chunksStorage->updateChunk($chunk);
$mutator->mutate($chunk);
$chunk->processingAccepted();
$this->chunksStorage->updateChunk($chunk);
}
catch (UnableToMutateChunkException $exception) {
$chunk->processingRejected();
$this->chunksStorage->updateChunk($chunk);
// Log the exception, maybe together with Chunk insert them into PostProcessing Queue
}
return false;
}
}
PhpUnit Prophecy solution
PhpUnit Prophecy 解决方案
class ProcessorTest extends ChunkTestCase
{
/**
* @var Processor
*/
private $processor;
/**
* @var MutatorResolver|ObjectProphecy
*/
private $mutatorResolverProphecy;
/**
* @var ChunksStorage|ObjectProphecy
*/
private $chunkStorage;
public function setUp()
{
$this->mutatorResolverProphecy = $this->prophesize(MutatorResolver::class);
$this->chunkStorage = $this->prophesize(ChunksStorage::class);
$this->processor = new Processor(
$this->mutatorResolverProphecy->reveal(),
$this->chunkStorage->reveal()
);
}
public function testProcessShouldPersistChunkInCorrectStatusBeforeAndAfterTheMutateOperation()
{
$self = $this;
// Chunk is always passed with ACK_BY_QUEUE status to process()
$chunk = $this->createChunk();
$chunk->ackByQueue();
$campaignMutatorMock = $self->prophesize(CampaignMutator::class);
$campaignMutatorMock
->mutate($chunk)
->shouldBeCalled();
$this->mutatorResolverProphecy
->resolve($chunk)
->shouldBeCalled()
->willReturn($campaignMutatorMock->reveal());
$this->chunkStorage
->updateChunk($chunk)
->shouldBeCalled()
->will(
function($args) use ($self) {
$chunk = $args[0];
$self->assertTrue($chunk->status() === Chunk::STATUS_PROCESSING_IN_PROGRESS);
$self->chunkStorage
->updateChunk($chunk)
->shouldBeCalled()
->will(
function($args) use ($self) {
$chunk = $args[0];
$self->assertTrue($chunk->status() === Chunk::STATUS_PROCESSING_UPLOAD_ACCEPTED);
return true;
}
);
return true;
}
);
$this->processor->process($chunk);
}
}
Summary
概括
Once again, Prophecy is more awesome! My trick is to leverage the messaging binding nature of Prophecy and even though it sadly looks like a typical, callback javascript hell code, starting with $self = $this;as you very rarely have to write unit tests like this I think it's a nice solution and it's definitely easy to follow, debug, as it actually describes the program execution.
再一次,预言更棒!我的技巧是利用 Prophecy 的消息绑定特性,尽管它看起来像一个典型的回调 javascript 地狱代码,以$self = $this;开头;由于您很少需要编写这样的单元测试,因此我认为这是一个不错的解决方案,并且绝对易于遵循和调试,因为它实际上描述了程序执行。
BTW: There is a second alternative but requires changing the code we are testing. We could wrap the troublemakers and move them to a separate class:
顺便说一句:还有第二种选择,但需要更改我们正在测试的代码。我们可以包装麻烦制造者并将它们移到一个单独的类中:
$chunk->processingInProgress();
$this->chunksStorage->updateChunk($chunk);
could be wrapped as:
可以包装为:
$processorChunkStorage->persistChunkToInProgress($chunk);
and that's it but as I didn't want to create another class for it, I prefer the first one.
就是这样,但由于我不想为它创建另一个类,我更喜欢第一个。