有没有办法为不同的输入参数定义不同的模拟期望?例如,我有一个名为 DB 的数据库层类。这个类有一个名为“Query ( string $query )”的方法,该方法在输入时采用 SQL 查询字符串。我可以为此类(DB)创建模拟并为取决于输入查询字符串的不同查询方法调用设置不同的返回值吗?
如果可以避免的话,使用
at()
并不理想,因为 正如他们的文档声称的那样
at() 匹配器的 $index 参数是指给定模拟对象的所有方法调用中从零开始的索引。使用此匹配器时请务必小心,因为它可能会导致与特定实现细节联系过于紧密的脆弱测试。
从 4.1 开始,您可以使用
withConsecutive
例如。
$mock->expects($this->exactly(2))
->method('set')
->withConsecutive(
[$this->equalTo('foo'), $this->greaterThan(0)],
[$this->equalTo('bar'), $this->greaterThan(0)]
);
如果您想让它在连续调用时返回:
$mock->method('set')
->withConsecutive([$argA1, $argA2], [$argB1], [$argC1, $argC2])
->willReturnOnConsecutiveCalls($retValueA, $retValueB, $retValueC);
PHPUnit 10 删除了
withConsecutive
。您可以获得类似的功能:
$mock->expects($this->exactly(2))
->method('set')
->willReturnCallback(fn (string $property, int $value) => match (true) {
$property === 'foo' && $value > 0,
$property === 'bar' && $value > 0 => $mock->$property = $value,
default => throw new LogicException()
});
显然更丑陋而且不完全相同,但这就是事情的现状。您可以在这里阅读有关替代方案的更多信息:https://github.com/sebastianbergmann/phpunit/issues/4026 在这里:https://github.com/sebastianbergmann/phpunit/issues/4026#issuecomment-825453794
PHPUnit 模拟库(默认情况下)仅根据传递给
expects
参数的匹配器和传递给 method
的约束来确定期望是否匹配。因此,仅传递给 expect
的参数不同的两个 with
调用将会失败,因为两者都会匹配,但只有一个会验证是否具有预期行为。请参阅实际工作示例后的再现案例。
对于您的问题,您需要使用
->at()
或->will($this->returnCallback(
,如another question on the subject
中所述。
<?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";
}
}
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)
<?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"));
}
}
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
根据我的发现,解决此问题的最佳方法是使用 PHPUnit 的值映射功能。
来自 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'));
}
}
此测试通过。正如你所看到的:
据我所知,此功能是在 PHPUnit 3.6 中引入的,因此它已经足够“老”了,可以安全地在几乎任何开发或登台环境以及任何持续集成工具中使用。
看来 Mockery (https://github.com/padraic/mockery) 支持这一点。就我而言,我想检查数据库上是否创建了 2 个索引:
嘲讽,有效:
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,失败了:
$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 也有更好的语法恕我直言。它似乎比 PHPUnits 内置的模拟功能慢一点,但是 YMMV。
我们正在尝试升级 PHP8.1 上的 Phpunit10 测试,作为我们图像/库的年度升级。
在 Phpunit10 上,at() 和 withConsecutive() 已弃用。
@Radu Murzea 的解决方案适用于大多数情况:不是我们的!
我需要模拟 MongoDB 调用:参数有时是 MongoDB\ObjectId; returnValueMap() 使用 === 来比较收到的参数:如果是对象,则比较失败,如 php 文档所述 php.net/manual/en/language.oop5.object-comparison.php
我模拟 MongoDB FindOne 的解决方案如下:
$map = [
[
['_id' => new ObjectId("5825cfc1316f54c6128b4572"),],
[],
['_id' => new ObjectId("5825cfc1316f54c6128b4572"), 'username' => 'test']
],
[
['agencyIds' => new ObjectId("5825cfc1316f54c6128b4572"),],
['_id'],
false
],
[
['agencyIds' => new ObjectId("5825cfc1316f54c6128b4572"),],
['_id'],
false
],
];
$mongoDBUsersCollectionMock = $this->createMock(MongoDBCollection::class);
$mongoDBUsersCollectionMock
->method('findOne')
->with($this->anything())
->will($this->returnCallback(
function($filter, $options) use (&$map){
list($mockedFilter, $mockedOptions, $mockedReturn) = array_shift($map);
// if contains object remember don't use === because mean the exactly the same object
// ref: https://www.php.net/manual/en/language.oop5.object-comparison.php
if ($filter == $mockedFilter && $options == $mockedOptions){
return $mockedReturn;
}
}
));
好吧,我看到为 Mockery 提供了一个解决方案,所以由于我不喜欢 Mockery,我将为您提供一个 Prophecy 替代方案,但我建议您首先先阅读 Mockery 和 Prophecy 之间的区别。
长话短说:“预言使用称为消息绑定的方法 - 这意味着该方法的行为不会随着时间的推移而改变,而是会被其他方法改变。”
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;
}
}
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);
}
}
预言再次更牛逼!我的技巧是利用 Prophecy 的消息传递绑定性质,尽管它看起来像一个典型的回调 javascript 地狱代码,以 $self = $this; 开头,因为你很少需要编写这样的单元测试,我认为它是一个很好的解决方案,它绝对很容易遵循和调试,因为它实际上描述了程序的执行。
顺便说一句:还有第二种选择,但需要更改我们正在测试的代码。我们可以将麻烦制造者包装起来,并将他们转移到一个单独的班级:
$chunk->processingInProgress();
$this->chunksStorage->updateChunk($chunk);
可以包装为:
$processorChunkStorage->persistChunkToInProgress($chunk);
就是这样,但由于我不想为它创建另一个类,所以我更喜欢第一个。