phpunit模拟方法多个调用不同的参数

有什么办法来为不同的input参数定义不同的模拟期望? 例如,我有数据库层类称为数据库。 这个类有一个叫做“Query(string $ query)”的方法,该方法在input时使用一个SQL查询string。 我可以为这个类(DB)创build模拟,并为依赖于input查询string的不同Query方法调用设置不同的返回值吗?

PHPUnit Mocking库(默认情况下)仅根据传递给expects参数的匹配器和传递给method的约束来确定期望值是否匹配。 因此,两个expect调用只会在传递给的参数上有所不同,因为两者都会匹配,但只有一个会被validation为具有预期的行为。 看到实际工作示例后的再现案例。


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

如果你可以避免使用at()这是不理想的,因为他们的文档声称

at()匹配器的$ index参数指向给定模拟对象的所有方法调用中从零开始的索引。 使用这个匹配器时要小心,因为它会导致脆弱的testing,这与特定的实现细节密切相关。

自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的值映射function。

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

这个testing通过。 如你看到的:

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

从我所知道的情况来看,这个特性是在PHPUnit 3.6中引入的,所以这个特性已经够老了,可以安全的用在几乎所有的开发或者分期环境中,并且可以使用任何持续集成工具。

看来Mockery( https://github.com/padraic/mockery )支持这个。 在我的情况下,我想检查2个索引是在数据库上创build的:

嘲笑,作品:

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

嘲笑也有更好的语法恕我直言。 它似乎比PHPUnits内置的模拟function慢一点,但是YMMV。

介绍

好吧,我看到有一个解决scheme提供给嘲笑,所以我不喜欢嘲笑,我会给你一个预言的select,但我会build议你先阅读嘲笑和预言之间的区别。

长话短说 :“预言使用称为消息绑定的方法 – 这意味着方法的行为不会随着时间而改变,而是由另一种方法改变。

真实的世界有问题的代码来覆盖

 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预言解决scheme

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

概要

预言再一次更加棒! 我的诀窍是利用预言的消息传递绑定性质,即使它可悲看起来像一个典型的,callbackJavaScript地狱代码,从$ self = $ this; 因为你很less需要编写像这样的unit testing,所以我认为这是一个很好的解决scheme,它很容易跟踪,debugging,因为它实际上描述了程序的执行。

顺便说一句:还有第二个select,但需要改变我们正在testing的代码。 我们可以包装麻烦制造者,并把他们转移到一个单独的课堂:

 $chunk->processingInProgress(); $this->chunksStorage->updateChunk($chunk); 

可以包装为:

 $processorChunkStorage->persistChunkToInProgress($chunk); 

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