PHPUnit макет метод несколько вызовов с разными аргументами
есть ли способ определить разные макетные ожидания для разных входных аргументов? Например, у меня есть класс уровня базы данных под названием DB. Этот класс имеет метод с именем " Query (string $query)", этот метод принимает строку запроса SQL на входе. Могу ли я создать макет для этого класса (DB) и установить разные возвращаемые значения для разных вызовов метода запроса, которые зависят от входной строки запроса?
5 ответов:
библиотека насмешек 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
это не идеально, чтобы использовать
at()
Если вы можете избежать его, потому что как утверждают их документыпараметр $index для AT () matcher ссылается на индекс, начиная с нуля, во всех вызовах метода для данного макетного объекта. Будьте осторожны при использовании этого сопоставителя, так как это может привести к хрупким тестам, которые слишком тесно связаны с конкретными деталями реализации.
С 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, поэтому он достаточно "стар", чтобы его можно было безопасно использовать практически в любой среде разработки или промежуточной среды и с любым инструментом непрерывной интеграции.
это кажется издевательством (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);
издевательство также имеет более приятный синтаксис ИМХО. Это, кажется, немного медленнее, чем phpunits встроенная насмешливая возможность, но YMMV.
интро
хорошо, я вижу, что есть одно решение, предусмотренное для издевательства, так как мне не нравится издевательство, я собираюсь дать вам альтернативу пророчеству, но я бы предложил вам сначала сначала прочитайте о разнице между насмешкой и пророчеством.
короче: "пророчество использует подход под названием сообщение обязывающего - это означает, что поведение метода не изменяется с течением времени, а скорее изменяется другим метод."
реальный мир проблемный код, чтобы покрыть
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
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); } }
резюме
еще раз, пророчество является более удивительным! Мой трюк заключается в том, чтобы использовать привязку сообщений к пророчеству, и хотя это, к сожалению, похоже на типичный код javascript hell с обратным вызовом, начиная с $self = $this; как вы очень редко приходится писать unit-тесты, как это я думаю, что это хорошее решение, и это наверняка легко следовать, отлаживать, как это на самом деле описывает выполнение программы.
кстати: есть вторая альтернатива, но требует изменения кода, который мы тестируем. Мы могли бы завернуть нарушителей спокойствия и переместить их в отдельный класс:
$chunk->processingInProgress(); $this->chunksStorage->updateChunk($chunk);
может быть обернут как:
$processorChunkStorage->persistChunkToInProgress($chunk);
и это все, но поскольку я не хотел создавать для него другой класс, я предпочитаю первый.