web-dev-qa-db-fra.com

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

Est-il possible de configurer la maquette PHPUnit de cette façon?

$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 il é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 vide la maquette, tout semble correct.

49
Václav Novotný

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

30
leeb

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

Je peux voir deux options qui peuvent vous rapprocher de quelque chose comme ça:

Utilisation de -> à ($ 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 vous ne devriez (principalement qu'il est appelé avec matcher en premier, et c'est un détail d'implémentation).

Cela échouera également si vous avez plus d'un appel à chacune des fonctions!


Accepter les deux paramètres et utiliser returnCallBack

C'est plus de travail mais ça marche mieux puisque vous ne dépendez pas de l'ordre des appels:

Exemple de travail:

<?php

class FooTest extends PHPUnit_Framework_TestCase {


    public function testX() {

        $context = $this->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());
                    // The first arg will be Matcher or Logger
                    // so something like "return new $param" should work here
                }
           ));

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


    }

}

class Context {

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

Cela produira:

/*
$ phpunit footest.php
PHPUnit 3.5.11 by 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 avec le comptage des invocations. Si vous n'en avez pas besoin, le remplacer par $this->any() fonctionnera, bien sûr.

61
edorian

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

Doit passer pour tout argument "Logger" ou "Matcher" passé à la méthode offsetGet du Context Mock:

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 dataProvider. Et dans chacun de ces tests, il a fait l'assertion pour with() et celle pour instanceOf, d'où quatre assertions.

7
Gordon

Dans le prolongement de la réponse de @edorian et des commentaires (@MarijnHuizendveld) concernant la garantie que la méthode est appelée avec Matcher et Logger, et pas 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]);
                }
                // The first arg will be Matcher or Logger
                // so something like "return new $param" should work here
            }
       ));

// perform actions...

// check all arguments removed
$this->assertEquals(array(), $expectedArguments, 'Method offsetGet not called with all required arguments');

C'est avec PHPUnit 3.7.

Si la méthode que vous testez ne renvoie rien, et que vous devez simplement tester qu'elle est appelée avec les arguments corrects, la même approche s'applique. Pour ce scénario, j'ai également tenté de le faire en utilisant une fonction de rappel pour $ this-> callback comme argument du with, plutôt que de returnCallback dans le testament. Cela échoue, car phpunit appelle en interne le rappel deux fois dans le processus de vérification du rappel de la correspondance d'arguments. Cela signifie que l'approche échoue car au deuxième appel, cet argument a déjà été supprimé du tableau d'arguments attendu. Je ne sais pas pourquoi phpunit l'appelle deux fois (cela semble un gaspillage inutile), et je suppose que vous pouvez contourner cela en ne le supprimant que lors du deuxième appel, mais je n'étais pas assez confiant que c'est un comportement phpunit voulu et cohérent pour compter sur ce qui se passe.

5
crysallus

Mes 2 cents au sujet: faites attention lorsque vous utilisez à ($ x): cela signifie que l'appel de méthode attendu sera le ($ x + 1) ème appel de méthode sur l'objet factice; cela ne signifie pas que ce sera le ($ x + 1) ème appel de la méthode attendue. Cela m'a fait perdre du temps, donc j'espère que ce ne sera pas avec vous. Cordialement à tous.

3
Alessandro Ronchi

Je suis juste tombé sur cette PHP pour se moquer des objets: https://github.com/etsy/phpunit-extensions/wiki/Mock-Object

2
powtac

Voici également quelques solutions avec la bibliothèque doublit :

Solution 1: en utilisant Stubs::returnValueMap

/* Get a dummy double instance  */
$double = Doublit::dummy_instance(Context::class);

/* Test the "offsetGet" method */
$double::_method('offsetGet')
    // Test that the first argument is equal to "Matcher" or "Logger"
    ->args([Constraints::logicalOr('Matcher', 'Logger')])
    // Return "new Matcher()" when first argument is "Matcher"
    // Return "new Logger()" when first argument is "Logger"
    ->stub(Stubs::returnValueMap([['Matcher'], ['Logger']], [new Matcher(), new Logger()]));

Solution 2: utiliser un rappel

/* Get a dummy double instance  */
$double = Doublit::dummy_instance(Context::class);

/* Test the "offsetGet" method */
$double::_method('offsetGet')
    // Test that the first argument is equal to "Matcher" or "Logger"
    ->args([Constraints::logicalOr('Matcher', 'Logger')])
    // Return "new Matcher()" when first argument $arg is "Matcher"
    // Return "new Logger()" when first argument $arg is "Logger"
    ->stub(function($arg){
        if($arg == 'Matcher'){
            return new Matcher();
        } else if($arg == 'Logger'){
            return new Logger();
        }
    });
0
gealex