Existe-t-il un moyen de définir différentes attentes fictives pour différents arguments d'entrée? Par exemple, j'ai une classe de couche de base de données appelée DB. Cette classe a une méthode appelée "Query (string $ query)", cette méthode prend une chaîne de requête SQL en entrée. Puis-je créer une maquette pour cette classe (DB) et définir différentes valeurs de retour pour différents appels de méthode Query qui dépendent de la chaîne de requête d'entrée?
La bibliothèque PHPUnit Mocking (par défaut) détermine si une attente est vérifiée uniquement en fonction du matcher transmis au paramètre expects
et de la contrainte transmise à method
. Pour cette raison, deux appels expect
dont les arguments transmis à with
ne diffèrent que par la suite, échoueront car les deux correspondront, mais un seul vérifiera le comportement attendu. Voir le cas de reproduction après l'exemple de travail réel.
Pour votre problème, vous devez utiliser ->at()
ou ->will($this->returnCallback(
comme indiqué dans 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
Ce n'est pas idéal d'utiliser at()
si vous pouvez l'éviter car comme le prétendent leurs documents
Le paramètre $ index du matcher at () fait référence à l'index, commençant à zéro, dans tous les appels de méthodes pour un objet factice donné. Faites preuve de prudence lorsque vous utilisez cet adaptateur car cela peut conduire à des tests fragiles trop étroitement liés à des détails d'implémentation spécifiques.
Depuis 4.1, vous pouvez utiliser withConsecutive
eg.
$mock->expects($this->exactly(2))
->method('set')
->withConsecutive(
[$this->equalTo('foo'), $this->greaterThan(0)],
[$this->equalTo('bar'), $this->greaterThan(0)]
);
Si vous voulez le faire revenir sur des appels consécutifs:
$mock->method('set')
->withConsecutive([$argA1, $argA2], [$argB1], [$argC1, $argC2])
->willReturnOnConsecutiveCalls($retValueA, $retValueB, $retValueC);
D'après ce que j'ai trouvé, le meilleur moyen de résoudre ce problème consiste à utiliser la fonctionnalité de cartographie des valeurs de PHPUnit.
Exemple de documentation de 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'));
}
}
Ce test réussit. Comme vous pouvez le voir:
D'après ce que je peux dire, cette fonctionnalité a été introduite dans PHPUnit 3.6 . environnements de transfert et avec n’importe quel outil d’intégration continue.
Il semble que Mockery ( https://github.com/padraic/mockery ) le supporte. Dans mon cas, je veux vérifier que 2 index sont créés sur une base de données:
Raillerie, travaux:
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, cela échoue:
$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 a également une syntaxe plus agréable IMHO. Cela semble être un peu plus lent que la capacité de moquage intégrée de PHPUnits, mais YMMV.
Ok, je vois qu’une solution est proposée pour Mockery. Par conséquent, comme je n’aime pas Mockery, je vais vous proposer une alternative à la Prophétie, mais je vous suggérerais d’abord de lisez en premier lieu la différence entre Mockery et Prophecy.
Longue histoire courte : "Prophecy utilise une approche appelée la liaison du message - cela signifie ce comportement de la méthode ne change pas au fil du temps, mais est modifié par l'autre méthode. "
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;
}
}
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);
}
}
Encore une fois, la prophétie est encore plus géniale! Mon astuce consiste à exploiter la nature de liaison de la messagerie de Prophecy et même si elle ressemble tristement à un code d’enfer javascript de rappel typique, commençant par $ self = $ this; comme vous devez très rarement écrire des tests unitaires comme celui-ci, je pense que c'est une solution intéressante et qu'il est vraiment facile à suivre, à déboguer, car cela décrit en fait l'exécution du programme.
BTW: Il existe une deuxième alternative mais nécessite de changer le code que nous testons. Nous pourrions envelopper les fauteurs de troubles et les déplacer dans une classe séparée:
$chunk->processingInProgress();
$this->chunksStorage->updateChunk($chunk);
pourrait être emballé comme:
$processorChunkStorage->persistChunkToInProgress($chunk);
et c'est tout, mais comme je ne voulais pas créer une autre classe, je préfère la première.