Phpunit mock method multiple calls with different arguments

Is there a way to define different mock expectations for different input arguments? For example, I have a database level class called DB. This class has a method called "Query (string $ query)", this method accepts an SQL query string as input. Can I create a mock for this class (DB) and set different return values ​​for different query method calls, which depend on the query input line?

+75
php mocking phpunit
May 13 '11 at 7:25
source share
5 answers

The PHPUnit Mocking library (by default) determines whether the expectation is a match based only on matches passed to the expects parameter and a constraint passed to method . Because of this, two calls to expect , which differ only in the arguments passed to with , will fail because both will match, but only one will be tested as having the expected behavior. See Playback Example After Actual Working Example.




For you, you need to use ->at() or ->will($this->returnCallback( as mentioned in 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"; } } 

Reproduced

 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 β†’ s () calls do not work:

 <?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 
+91
May 13 '11 at 8:14
source share

It is not possible to use at() if you can avoid this, because as their docs state

The $ index parameter for the at () attribute refers to the index, starting from zero, in all method calls for this layout. Use caution when using this connector, as this can lead to fragile tests that are too closely related to specific implementation details.

Starting with version 4.1 you can use withConsecutive for example.

 $mock->expects($this->exactly(2)) ->method('set') ->withConsecutive( [$this->equalTo('foo'), $this->greaterThan(0)], [$this->equalTo('bar'), $this->greaterThan(0)] ); 

If you want it to return on successive calls:

  $mock->method('set') ->withConsecutive([$argA1, $argA2], [$argB1], [$argC1, $argC2]) ->willReturnOnConsecutiveCalls($retValueA, $retValueB, $retValueC); 
+75
May 29 '14 at 21:04
source share

From what I found, the best way to solve this problem is to use the functionality of the map functions of PHPUnit.

An example from the PHPUnit documentation :

 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 a function is called with parameters "a" and "b", "d" is returned
  • when a function is called with parameters "e" and "f", "h" is returned

From what I can say, this function was introduced in PHPUnit 3.6 , so it is "old" enough to be safe to use in almost any development environment or intermediate level and using any continuous integration tool.

+9
Jan 13 '15 at 16:13
source share

Mockery ( https://github.com/padraic/mockery ) seems to support this. In my case, I want to check that 2 indexes are created in the database:

Locations, 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:

 $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 better IMHO syntax. It seems like it is a bit slower than the built-in function of PHPUnits, but YMMV.

+3
May 02 '14 at 4:15
source share

Introduction

Well, I see that there is one solution for Mockery, since I do not like Mockery, I will give you an alternative to Prophecy, but first I suggest you read about the difference between Mockery and Prophecy first.

In short : "Prophecy takes a binding approach - this means that the behavior of the method does not change over time, but rather changes by another method. "

Real world problem 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

 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

Again, the Prophecy is more amazing! My trick is to use the connecting nature of the Prophecy, and although this unfortunately looks like a typical, add-on javascript callback, starting with $ self = $ this; , since you very rarely have to write unit tests. I think this is a pleasant solution, and it is definitely easy to track, debug, because it actually describes the execution of the program.

BTW: There is a second alternative, but it requires changing the code that we are testing. We could fool troublemakers and move them to a separate class:

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

can be wrapped as:

 $processorChunkStorage->persistChunkToInProgress($chunk); 

and that he, but since I did not want to create another class for him, I prefer the first.

0
Aug 04 '17 at 15:21
source share



All Articles