具有不同参数的多个调用

有没有办法为不同的输入参数定义不同的模拟期望?例如,我有一个名为 DB 的数据库层类。该类有一个名为“ Query (string $Query)”的方法,该方法在输入时接受一个 SQL 查询字符串。我是否可以为这个类(DB)创建 mock,并根据输入查询字符串为不同的 Query 方法调用设置不同的返回值?

127780 次浏览

PHPUnit Mocking 库(默认情况下)仅根据传递给 expects参数的匹配器和传递给 method的约束来确定期望是否匹配。因此,两个仅在传递给 with的参数上有差异的 expect调用将失败,因为两个调用都将匹配,但只有一个调用将验证是否具有预期的行为。请参见实际工作示例之后的复制实例。


对于您的问题,您需要使用 ->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)


再现为什么 two-> 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"));
}


}

结果出来了

 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

似乎嘲笑(https://github.com/padraic/mockery)支持这一点。在我的例子中,我想检查两个索引是否创建在一个数据库上:

嘲笑,作品:

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

恕我直言,嘲笑还有一个更好的语法。它似乎比 PHPUnit 内置的模拟能力慢一点,但是 YMMV。

如果可以避免使用 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 的值映射功能。

来自 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'));
}
}

这个测试通过了,如你所见:

  • 当使用参数“ a”和“ b”调用函数时,返回“ d”
  • 当使用参数“ e”和“ f”调用函数时,返回“ h”

据我所知,这个特性是在 PHPUnit 3.6中引入的,所以它已经足够“老”了,可以安全地用于几乎任何开发或临时环境,以及任何持续集成工具。

介绍

好吧,我看到有一个解决方案提供给嘲笑,所以我不喜欢嘲笑,我要给你一个预言的替代品,但我会建议你首先到 先读嘲笑和预言的区别。

长话短说 : “预言使用的方法称为 消息绑定-这意味着方法的行为不会随着时间的推移而改变,而是被其他方法所改变。”

需要覆盖的现实问题代码

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

就是这样,但因为我不想为它创建另一个类,所以我更喜欢第一个类。