phpunit макет метод несколько вызовов с различными аргументами
Существует ли какой-либо способ определить различные макетные ожидания для различных входных аргументов? Например, у меня есть класс слоя базы данных DB. Этот класс имеет метод под названием "Query (string $query)", который принимает строку SQL-запроса на вход. Можно ли создать макет для этого класса (БД) и задать различные возвращаемые значения для различных вызовов метода запроса, которые зависят от входной строки запроса?
5 ответов:
Библиотека PHPUnit Mocking (по умолчанию) определяет, соответствует ли ожидание, основанное исключительно на параметре matcher, переданном в
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() ссылается на индекс, начинающийся с нуля, во всех вызовах метода для данного макетного объекта. Будьте осторожны при использовании этого сопоставления, так как оно может привести к хрупким тестам, которые слишком тесно связаны с конкретными деталями реализации.
Начиная с 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 для обратного вызова, начиная с $self = $this; поскольку вам очень редко приходится писать модульные тесты, подобные этому, я думаю, что это хорошее решение, и его определенно легко выполнить, отладить, так как оно на самом деле описывает программу. исполнение.
Кстати: есть и вторая альтернатива, но она требует изменения кода, который мы тестируем. Мы могли бы завернуть нарушителей спокойствия и переместить их в отдельный класс:
$chunk->processingInProgress(); $this->chunksStorage->updateChunk($chunk);Можно обернуть следующим образом:
$processorChunkStorage->persistChunkToInProgress($chunk);И это все, но поскольку я не хотел создавать для него другой класс, я предпочитаю первый.
Comments