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?


En plus de la réponse ci-dessous, vous pouvez également utiliser la méthode dans cette réponse: stackoverflow.com/questions/5484602/…
Schleis

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

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

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.


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.


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.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.