Есть ли способ определить разные mock-ожидания для разных входных аргументов? Например, у меня есть класс уровня базы данных, называемый DB. Этот класс имеет метод под названием "Query (string $query)", этот метод принимает строку запроса SQL на входе. Могу ли я создать mock для этого класса (DB) и установить разные значения возврата для разных вызовов метода запросов, которые зависят от строки ввода запроса?
Phpunit mock метод несколько вызовов с разными аргументами
Ответ 1
Библиотека PHPUnit Mocking (по умолчанию) определяет, будет ли математическое ожидание совпадением, основанным только на матчи, переданном параметру 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
Ответ 2
Невозможно использовать 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);
Ответ 3
Из того, что я нашел, лучший способ решить эту проблему - использовать функциональные возможности map-функций 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, поэтому она "старая" достаточно, чтобы ее можно было безопасно использовать практически в любых средах разработки или промежуточного уровня и с помощью любого инструмента непрерывной интеграции.
Ответ 4
Кажется, 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.
Ответ 5
Введение
Хорошо, я вижу, что есть одно решение для 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);
}
}
Резюме
Опять же, Пророчество более удивительно! Мой трюк заключается в том, чтобы использовать связующую природу Пророчества, и хотя это, к сожалению, выглядит типичным, аддовым кодом обратного вызова javascript, начиная с $self = $this;, поскольку вам очень редко приходится писать модульные тесты Я думаю, что это приятное решение, и это определенно легко отслеживать, отлаживать, поскольку оно фактически описывает выполнение программы.
BTW: Существует вторая альтернатива, но требует изменения кода, который мы тестируем. Мы могли бы обмануть нарушителей спокойствия и переместить их в отдельный класс:
$chunk->processingInProgress();
$this->chunksStorage->updateChunk($chunk);
может быть завернуто как:
$processorChunkStorage->persistChunkToInProgress($chunk);
и что он, но поскольку я не хотел создавать для него еще один класс, я предпочитаю первый.