phpunit mock méthode plusieurs appels avec différents arguments

117

Existe-t-il un moyen de définir différentes fausses attentes 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 des valeurs de retour différentes pour différents appels de méthode de requête qui dépendent de la chaîne de requête d'entrée?

Aleksei Kornushkin
la source
En plus de la réponse ci-dessous, vous pouvez également utiliser la méthode dans cette réponse: stackoverflow.com/questions/5484602/…
Schleis
J'aime cette réponse stackoverflow.com/a/10964562/614709
yitznewton

Réponses:

132

La bibliothèque PHPUnit Mocking (par défaut) détermine si une attente correspond uniquement en fonction du matcher passé au expectsparamètre et de la contrainte transmise à method. Pour cette raison, deux expectappels qui ne diffèrent que par les arguments passés à withéchoueront car les deux correspondront mais un seul vérifiera comme ayant 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.

Exemple:

<?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";
    }
}

Reproduit:

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)


Reproduisez pourquoi deux appels -> with () ne fonctionnent pas:

<?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"));
    }

}

Résulte en

 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
édorien
la source
7
Merci de votre aide! Votre réponse a complètement résolu mon problème. PS Parfois, le développement de TDD me semble terrifiant lorsque je dois utiliser des solutions aussi volumineuses pour une architecture simple :)
Aleksei Kornushkin
1
C'est une excellente réponse, qui m'a vraiment aidé à comprendre les simulations de PHPUnit. Merci!!
Steve Bauman
Vous pouvez également utiliser $this->anything()comme l'un des paramètres pour ->logicalOr()vous permettre de fournir une valeur par défaut pour d'autres arguments que celui qui vous intéresse.
MatsLindh
2
Je me demande que personne ne mentionne qu'avec "-> logicalOr ()" vous ne garantissez pas que (dans ce cas) les deux arguments ont été appelés. Cela ne résout donc pas vraiment le problème.
user3790897
184

Ce n'est pas idéal à utiliser at()si vous pouvez l'éviter car, comme le prétendent leurs documents

Le paramètre $ index pour le matcher at () fait référence à l'index, commençant à zéro, dans toutes les invocations de méthode pour un objet fictif donné. Soyez prudent lorsque vous utilisez ce matcher car il peut conduire à des tests fragiles qui sont trop étroitement liés à des détails d'implémentation spécifiques.

Depuis 4.1, vous pouvez utiliser withConsecutivepar exemple.

$mock->expects($this->exactly(2))
     ->method('set')
     ->withConsecutive(
         [$this->equalTo('foo'), $this->greaterThan(0)],
         [$this->equalTo('bar'), $this->greaterThan(0)]
       );

Si vous souhaitez le faire revenir sur des appels consécutifs:

  $mock->method('set')
         ->withConsecutive([$argA1, $argA2], [$argB1], [$argC1, $argC2])
         ->willReturnOnConsecutiveCalls($retValueA, $retValueB, $retValueC);
Hirowatari
la source
22
Meilleure réponse en 2016. Meilleure que la réponse acceptée.
Matthew Housser
Comment retourner quelque chose de différent pour ces deux paramètres différents?
Lenin Raj Rajasekaran
@emaillenin en utilisant willReturnOnConsecutiveCalls de la même manière.
xarlymg89
Pour info, j'utilisais PHPUnit 4.0.20 et recevais une erreur Fatal error: Call to undefined method PHPUnit_Framework_MockObject_Builder_InvocationMocker::withConsecutive(), mis à niveau vers la version 4.1 en un clin d'œil avec Composer et cela fonctionne.
quickshift du
Ils l'ont willReturnOnConsecutiveCallstué.
Rafael Barros
18

D'après ce que j'ai trouvé, la meilleure façon de résoudre ce problème est d'utiliser la fonctionnalité de carte de valeur de PHPUnit.

Exemple tiré de la 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:

  • lorsque la fonction est appelée avec les paramètres "a" et "b", "d" est renvoyé
  • lorsque la fonction est appelée avec les paramètres "e" et "f", "h" est renvoyé

D'après ce que je peux dire, cette fonctionnalité a été introduite dans PHPUnit 3.6 , elle est donc suffisamment "ancienne" pour pouvoir être utilisée en toute sécurité sur à peu près n'importe quel environnement de développement ou de préparation et avec n'importe quel outil d'intégration continue.

Radu Murzea
la source
6

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:

La dérision, fonctionne:

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 plus belle syntaxe à mon humble avis. Cela semble être un peu plus lent que la capacité de simulation intégrée de PHPUnits, mais YMMV.

Joerx
la source
0

Intro

D'accord, je vois qu'une solution est fournie pour Mockery, donc comme je n'aime pas Mockery, je vais vous donner une alternative à Prophecy mais je vous suggère d'abord de lire la différence entre Mockery et Prophecy.

En bref : "La prophétie utilise une approche appelée liaison de message - cela signifie que le comportement de la méthode ne change pas avec le temps, mais est plutôt changé par l'autre méthode."

Code problématique du monde réel à couvrir

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;
    }
}

Solution 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);
    }
}

Résumé

Encore une fois, Prophecy est plus impressionnant! Mon astuce est de tirer parti de la nature de liaison de messagerie de Prophecy et même si cela ressemble malheureusement à 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 bonne solution et c'est vraiment facile à suivre, à déboguer, car il 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 distincte:

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

pourrait être enveloppé comme:

$processorChunkStorage->persistChunkToInProgress($chunk);

et c'est tout mais comme je ne voulais pas créer une autre classe pour cela, je préfère la première.

Lukas Lukac
la source