51 votes

Mock dans PHPUnit - configuration multiple de la même méthode avec des arguments différents

Est-il possible de configurer le mock PHPUnit de cette manière ?

$context = $this->getMockBuilder('Context')
   ->getMock();
$context->expects($this->any())
   ->method('offsetGet')
   ->with('Matcher')
   ->will($this->returnValue(new Matcher()));
$context->expects($this->any())
   ->method('offsetGet')
   ->with('Logger')
   ->will($this->returnValue(new Logger()));

J'utilise PHPUnit 3.5.10 et cela échoue lorsque je demande Matcher car il attend l'argument "Logger". C'est comme si la deuxième attente réécrivait la première, mais quand je fais un dump du mock, tout semble correct.

64voto

edorian Points 22780

Malheureusement, cela n'est pas possible avec l'API de simulation par défaut de PHPUnit.

Je peux voir deux options qui peuvent vous rapprocher de quelque chose de similaire :

Utiliser -> at($x)

$context = $this->getMockBuilder('Context')
   ->getMock();

$context->expects($this->at(0))
   ->method('offsetGet')
   ->with('Matcher')
   ->will($this->returnValue(new Matcher()));

$context->expects($this->at(1))
   ->method('offsetGet')
   ->with('Logger')
   ->will($this->returnValue(new Logger()));

Cela fonctionnera bien, mais vous testez plus que ce que vous devriez (principalement qu'il est appelé avec le matcher en premier, ce qui est un détail d'implémentation).

Aussi, cela échouera si vous avez plus d'un appel à chacune des fonctions !


Accepter les deux paramètres et utiliser returnCallBack

C'est plus de travail mais fonctionne mieux car vous ne dépendez pas de l'ordre des appels :

Exemple de travail :

getMockBuilder('Context')
           ->getMock();

        $context->expects($this->exactly(2))
           ->method('offsetGet')
           ->with($this->logicalOr(
                     $this->equalTo('Matcher'), 
                     $this->equalTo('Logger')
            ))
           ->will($this->returnCallback(
                function($param) {
                    var_dump(func_get_args());
                    // Le premier argument sera Matcher ou Logger
                    // donc quelque chose comme "return new $param" devrait fonctionner ici
                }
           ));

        $context->offsetGet("Matcher");
        $context->offsetGet("Logger");

    }

}

class Context {

    public function offsetGet() { echo "org"; }
}

Cela produira la sortie suivante :

/*
$ phpunit footest.php
PHPUnit 3.5.11 par Sebastian Bergmann.

array(1) {
  [0]=>
  string(7) "Matcher"
}
array(1) {
  [0]=>
  string(6) "Logger"
}
.
Time: 0 seconds, Memory: 3.00Mb

OK (1 test, 1 assertion)

J'ai utilisé $this->exactly(2) dans le matcher pour montrer que cela fonctionne également en comptant les invocations. Si vous n'avez pas besoin de cela, le remplacer par $this->any() fonctionnera bien sûr.

1 votes

Grande solution! Je suis d'accord avec vous sur le fait que l'ordre des appels ne doit pas transparaître dans les tests. Cependant, avec la deuxième approche, vous n'avez aucune garantie que la méthode a été appelée une fois avec chaque valeur attendue individuellement. Autrement dit, dans votre exemple, le code pourrait appeler deux fois le contexte avec Matcher en argument et jamais avec Logger en argument et cela passerait toujours. Selon le test, cela pourrait poser problème. Comment contournez-vous cela sans révéler l'ordre dans le cas de test?

2 votes

@MarijnHuizendveld "Une fermeture qui lie un tableau d'appels attendus, supprime tous ceux qu'elle a vus et un assertEmpty à la fin du test" serait la première chose qui me viendrait à l'esprit.

2 votes

Notez que ->at($x) inclut des appels de fonction à d'autres méthodes, donc si vous moquez une autre méthode en premier, peu importe qu'il s'agisse de la même méthode ou qu'il utilise ->at($x) lui-même, $x commence à 1 au lieu de 0.

31voto

leeb Points 302

Dès PHPUnit 3.6, il existe $this->returnValueMap() qui peut être utilisé pour retourner différentes valeurs en fonction des paramètres donnés à la méthode stub.

2 votes

Oui, vous pouvez utiliser returnValueMap() mais assurez-vous de spécifier tous les paramètres nécessaires à la méthode sinon cela ne fonctionne pas correctement. Voir github.com/sebastianbergmann/phpunit-mock-objects/issues/89

8voto

Gordon Points 156415

Vous pouvez y parvenir avec un rappel :

class MockTest extends PHPUnit_Framework_TestCase
{
    /**
     * @dataProvider provideExpectedInstance
     */
    public function testMockReturnsInstance($expectedInstance)
    {
        $context = $this->getMock('Context');

        $context->expects($this->any())
           ->method('offsetGet')
           // Accept any of "Matcher" or "Logger" for first argument
           ->with($this->logicalOr(
                $this->equalTo('Matcher'),
                $this->equalTo('Logger')
           ))
           // Return what was passed to offsetGet as a new instance
           ->will($this->returnCallback(
               function($arg1) {
                   return new $arg1;
               }
           ));

       $this->assertInstanceOf(
           $expectedInstance,
           $context->offsetGet($expectedInstance)
       );
    }
    public function provideExpectedInstance()
    {
        return array_chunk(array('Matcher', 'Logger'), 1);
    }
}

Devrait passer pour des arguments "Logger" ou "Matcher" passés à la méthode offsetGet du Mock de Context :

F:\Work\code\gordon\sandbox>phpunit NewFileTest.php
PHPUnit 3.5.13 by Sebastian Bergmann.

..

Time: 0 seconds, Memory: 3.25Mb

OK (2 tests, 4 assertions)

Comme vous pouvez le voir, PHPUnit a exécuté deux tests. Un pour chaque valeur de dataProvider. Et dans chacun de ces tests, il a fait l'assertion pour with() et celle pour instanceOf, d'où quatre assertions.

5voto

crysallus Points 145

Suite à la réponse de @edorian et aux commentaires (@MarijnHuizendveld) concernant le fait de s'assurer que la méthode est appelée à la fois avec Matcher et Logger, et non simplement deux fois avec Matcher ou Logger, voici un exemple.

$expectedArguments = array('Matcher', 'Logger');
$context->expects($this->exactly(2))
       ->method('offsetGet')
       ->with($this->logicalOr(
                 $this->equalTo('Matcher'), 
                 $this->equalTo('Logger')
        ))
       ->will($this->returnCallback(
            function($param) use (&$expectedArguments){
                if(($key = array_search($param, $expectedArguments)) !== false) {
                    // remove called argument from list
                    unset($expectedArguments[$key]);
                }
                // Le premier paramètre sera Matcher ou Logger
                // donc quelque chose comme "return new $param" devrait fonctionner ici
            }
       ));

// réaliser des actions...

// vérifier que tous les arguments ont été supprimés
$this->assertEquals(array(), $expectedArguments, 'La méthode offsetGet n\'a pas été appelée avec tous les arguments requis');

Ceci est avec PHPUnit 3.7.

Si la méthode que vous testez ne retourne en fait rien, et que vous devez simplement tester si elle est appelée avec les bons arguments, la même approche s'applique. Pour ce scénario, j'ai également essayé de le faire en utilisant une fonction de rappel pour $this->callback comme argument du with, plutôt que returnCallback dans le will. Cela échoue, car PHPUnit appelle la fonction de rappel deux fois dans le processus de vérification du callback de l'argument. Cela signifie que l'approche échoue car lors du deuxième appel, cet argument a déjà été supprimé du tableau des arguments attendus. Je ne sais pas pourquoi PHPUnit l'appelle deux fois (cela semble être un gaspillage inutile), et je suppose que vous pourriez contourner cela en ne le supprimant que lors du deuxième appel, mais je n'étais pas assez confiant que cela soit intentionnel et un comportement cohérent de PHPUnit pour me fier à cela.

0 votes

Merci beaucoup d'avoir pris le temps de taper un exemple! Très utile. :)

2voto

powtac Points 18619

Je viens de tomber sur cette extension PHP pour simuler des objets: https://github.com/etsy/phpunit-extensions/wiki/Mock-Object

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