phpunit mock метод несколько вызовов с разными аргументами

Есть ли способ определить разные mock-ожидания для разных входных аргументов? Например, у меня есть класс уровня базы данных, называемый DB. Этот класс имеет метод под названием «Query (string $ query)», этот метод принимает строку запроса SQL на входе. Могу ли я создать mock для этого класса (DB) и установить разные значения возврата для разных запросов метода запросов, которые зависят от строки ввода запроса?

Библиотека PHPUnit Mocking (по умолчанию) определяет, совпадает ли соответствие ожидания только с переданным матчи, который expects параметр и ограничение, переданное method . Из-за этого два expect , 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 

Это не идеально подходит для использования at() если вы можете избежать этого, потому что, поскольку их документы требуют

Параметр $ index для атрибута at () относится к индексу, начиная с нуля, во всех вызовах метода для данного макета. Соблюдайте осторожность при использовании этого соединителя, поскольку это может привести к хрупким испытаниям, которые слишком тесно связаны с конкретными деталями реализации.

Начиная с 4.1 вы можете использовать с.

 $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 , поэтому она «старая» настолько, что ее можно безопасно использовать практически во всех средах разработки или промежуточного уровня и с помощью любого инструмента непрерывной интеграции.

Кажется, 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 также имеет более хороший синтаксис IMHO. Похоже, что он немного медленнее, чем встроенная функция PHPUnits, но YMMV.

вступление

Хорошо, я вижу, что есть одно решение для 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; } } 

Решение 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); } } 

Резюме

Еще раз, Пророчество является более удивительным! Мой трюк заключается в том, чтобы использовать связующий характер сообщений Prophecy, и хотя это, к сожалению, похоже на типичный код кода JavaScript обратного вызова, начиная с $ self = $ this; так как вам очень редко приходится писать модульные тесты, как это, я думаю, что это приятное решение, и его, безусловно, легко отслеживать, отлаживать, поскольку он фактически описывает выполнение программы.

BTW: Существует вторая альтернатива, но требует изменения кода, который мы тестируем. Мы могли бы обмануть нарушителей спокойствия и перенести их в отдельный класс:

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

может быть завернута как:

 $processorChunkStorage->persistChunkToInProgress($chunk); 

и это все, но поскольку я не хотел создавать для него еще один класс, я предпочитаю первый.