138 votes

méthode fantaisie phpunit : appels multiples avec des arguments différents

Existe-t-il un moyen de définir des attentes fictives différentes pour des arguments d'entrée différents ? Par exemple, j'ai une classe de couche de base de données appelée DB. Cette classe possède une méthode appelée "Query ( string $query )", qui prend une chaîne de requête SQL en entrée. Puis-je créer un objet fantaisie pour cette classe (DB) et définir différentes valeurs de retour pour les différents appels de la méthode Query qui dépendent de la chaîne de requête en entrée ?

0 votes

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

0 votes

J'aime cette réponse stackoverflow.com/a/10964562/614709

232voto

hirowatari Points 11

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

Le paramètre $index pour le matcheur at() fait référence à l'index, en commençant à zéro, dans toutes les invocations de méthodes pour un objet fantaisie donné. Soyez prudent lors de l'utilisation de cet identificateur car il peut conduire à des tests fragiles qui sont trop étroitement liés à des détails d'implémentation spécifiques.

Depuis la version 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 qu'il revienne sur des appels consécutifs :

  $mock->method('set')
         ->withConsecutive([$argA1, $argA2], [$argB1], [$argC1, $argC2])
         ->willReturnOnConsecutiveCalls($retValueA, $retValueB, $retValueC);

32 votes

Meilleure réponse en 2016. Meilleure que la réponse acceptée.

0 votes

Comment renvoyer quelque chose de différent pour ces deux paramètres différents ?

0 votes

@emaillenin utilise willReturnOnConsecutiveCalls de manière similaire.

145voto

edorian Points 22780

La bibliothèque PHPUnit Mocking (par défaut) détermine si une attente correspond uniquement au matcher passé à la fonction expects et la contrainte passée à method . Pour cette raison, deux expect qui ne diffèrent que par les arguments passés à with échouera parce que les deux correspondront mais un seul vérifiera qu'il a 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";
    }
}

Se 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ésultats 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

8 votes

Merci pour votre aide ! Votre réponse a complètement résolu mon problème. P.S. Parfois, le développement TDD me semble terrifiant lorsque je dois utiliser des solutions aussi importantes pour une architecture simple :)

1 votes

C'est une excellente réponse, qui m'a vraiment aidé à comprendre les mocks de PHPUnit. Merci !

0 votes

Vous pouvez également utiliser $this->anything() comme l'un des paramètres de ->logicalOr() pour vous permettre de fournir une valeur par défaut pour d'autres arguments que celui qui vous intéresse.

25voto

Radu Murzea Points 4716

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

Exemple 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 est réussi. Comme vous pouvez le voir :

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

D'après ce que je peux dire, cette fonctionnalité a été introduite dans PHPUnit 3.6 Il est donc suffisamment "ancien" pour pouvoir être utilisé en toute sécurité dans pratiquement tous les environnements de développement ou de mise à disposition et avec tous les outils d'intégration continue.

6voto

joerx Points 525

Il semble que la moquerie ( https://github.com/padraic/mockery ) le confirme. Dans mon cas, je veux vérifier que 2 index sont créés sur une base de données :

La moquerie, ça marche :

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. Il semble qu'il soit un peu plus lent que la fonction d'asservissement intégrée de PHPUnits, mais c'est votre avis.

-1voto

EnchanterIO Points 2135

Intro

Ok je vois qu'il y a une solution 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 lisez d'abord la différence entre la moquerie et la prophétie.

Pour faire court : "La prophétie utilise une approche appelée liaison de messages - cela signifie que le comportement de la méthode ne change pas avec le temps, mais qu'il est plutôt 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 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 encore plus génial ! Mon astuce consiste à tirer parti de la nature de la liaison par messagerie de Prophecy et, même si cela ressemble tristement à un code typique de l'enfer de callback javascript, commencer par $self = $this ; Comme il est très rare d'avoir à écrire des tests unitaires de ce type, je pense que c'est une bonne solution et qu'elle est vraiment facile à suivre et à déboguer, car elle décrit réellement l'exécution du programme.

BTW : Il existe une deuxième alternative mais elle nécessite de modifier le code que nous testons. Nous pourrions envelopper les fauteurs de trouble et les déplacer dans une classe distincte :

$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 pour cela, je préfère la première.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X