web-dev-qa-db-fra.com

phpunit mock method plusieurs appels avec des arguments différents

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?

105

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 .

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)


Reproduire pourquoi deux -> avec appels () 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
120
edorian

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

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:

  • 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 . environnements de transfert et avec n’importe quel outil d’intégration continue.

12
Radu Murzea

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.

5
joerx

Intro

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. "

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 de prophétie 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);
    }
}

Sommaire

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.

0
BlocksByLukas