web-dev-qa-db-fra.com

Les meilleures méthodes pour tester les méthodes protégées avec PHPUnit

J'ai trouvé la discussion sur testez-vous la méthode privée informative.

J'ai décidé que dans certaines classes, je veux avoir des méthodes protégées, mais les tester. Certaines de ces méthodes sont statiques et courtes. Étant donné que la plupart des méthodes publiques les utilisent, je serai probablement en mesure de supprimer les tests en toute sécurité plus tard. Mais pour commencer avec une approche TDD et éviter le débogage, je veux vraiment les tester.

J'ai pensé à ce qui suit:

  • Objet Méthode comme indiqué dans ne réponse semble être excessif pour cela.
  • Commencez avec des méthodes publiques et, lorsque la couverture de code est donnée par des tests de niveau supérieur, tournez-les et supprimez les tests.
  • Hériter d'une classe avec une interface testable rendant les méthodes protégées publiques

Quelle est la meilleure pratique? Y a-t-il autre chose?

Il semble que JUnit change automatiquement les méthodes protégées pour qu'elles soient publiques, mais je ne les ai pas approfondies. PHP ne permet pas cela via réflexion .

268
GrGr

Si vous utilisez PHP5 (> = 5.3.2) avec PHPUnit, vous pouvez tester vos méthodes privées et protégées en utilisant la réflexion pour les rendre publiques avant d'exécuter vos tests:

protected static function getMethod($name) {
  $class = new ReflectionClass('MyClass');
  $method = $class->getMethod($name);
  $method->setAccessible(true);
  return $method;
}

public function testFoo() {
  $foo = self::getMethod('foo');
  $obj = new MyClass();
  $foo->invokeArgs($obj, array(...));
  ...
}
394
uckelman

Vous semblez déjà être au courant, mais je vais le reformuler quand même; C'est un mauvais signe si vous devez tester des méthodes protégées. Le but d'un test unitaire est de tester l'interface d'une classe. Les méthodes protégées sont des détails d'implémentation. Cela dit, il y a des cas où cela a du sens. Si vous utilisez l'héritage, vous pouvez voir une superclasse fournissant une interface à la sous-classe. Donc ici, vous devriez tester la méthode protégée (mais jamais une privée une). La solution à cela consiste à créer une sous-classe à des fins de test et à l'utiliser pour exposer les méthodes. Par exemple.:

class Foo {
  protected function stuff() {
    // secret stuff, you want to test
  }
}

class SubFoo extends Foo {
  public function exposedStuff() {
    return $this->stuff();
  }
}

Notez que vous pouvez toujours remplacer l'héritage par la composition. Lorsque vous testez du code, il est généralement beaucoup plus facile de traiter avec du code qui utilise ce modèle, vous pouvez donc envisager cette option.

46
troelskn

teastburn a la bonne approche. Encore plus simple, appelez directement la méthode et renvoyez la réponse:

class PHPUnitUtil
{
  public static function callMethod($obj, $name, array $args) {
        $class = new \ReflectionClass($obj);
        $method = $class->getMethod($name);
        $method->setAccessible(true);
        return $method->invokeArgs($obj, $args);
    }
}

Vous pouvez appeler cela simplement dans vos tests en:

$returnVal = PHPUnitUtil::callMethod(
                $this->object,
                '_nameOfProtectedMethod', 
                array($arg1, $arg2)
             );
35
robert.egginton

Je voudrais proposer une légère variation à getMethod () définie dans la réponse de uckelman .

Cette version modifie getMethod () en supprimant les valeurs codées en dur et en simplifiant un peu l'utilisation. Je recommande de l'ajouter à votre classe PHPUnitUtil comme dans l'exemple ci-dessous ou à votre classe PHPUnit_Framework_TestCase (ou, je suppose, globalement à votre fichier PHPUnitUtil).

Comme MyClass est de toute façon instancié et que ReflectionClass peut prendre une chaîne ou un objet ...

class PHPUnitUtil {
    /**
     * Get a private or protected method for testing/documentation purposes.
     * How to use for MyClass->foo():
     *      $cls = new MyClass();
     *      $foo = PHPUnitUtil::getPrivateMethod($cls, 'foo');
     *      $foo->invoke($cls, $...);
     * @param object $obj The instantiated instance of your class
     * @param string $name The name of your private/protected method
     * @return ReflectionMethod The method you asked for
     */
    public static function getPrivateMethod($obj, $name) {
      $class = new ReflectionClass($obj);
      $method = $class->getMethod($name);
      $method->setAccessible(true);
      return $method;
    }
    // ... some other functions
}

J'ai également créé une fonction alias getProtectedMethod () pour indiquer explicitement ce à quoi on s'attend, mais c'est à vous de décider.

À votre santé!

20
teastburn

Je pense que Troelskn est proche. Je ferais ceci à la place:

class ClassToTest
{
   protected testThisMethod()
   {
     // Implement stuff here
   }
}

Ensuite, implémentez quelque chose comme ceci:

class TestClassToTest extends ClassToTest
{
  public testThisMethod()
  {
    return parent::testThisMethod();
  }
}

Vous exécutez ensuite vos tests contre TestClassToTest.

Il devrait être possible de générer automatiquement de telles classes d'extension en analysant le code. Je ne serais pas surpris si PHPUnit offre déjà un tel mécanisme (bien que je n’aie pas vérifié).

9
Michael Johnson

Je vais jeter mon chapeau dans le ring ici:

J'ai utilisé le hack __call avec des succès mitigés. L’alternative que j’ai imaginée était d’utiliser le modèle de visiteur:

1: générer une classe stdClass ou personnalisée (pour appliquer le type)

2: amorcez avec la méthode et les arguments requis

3: assurez-vous que votre SUT a une méthode acceptVisitor qui l'exécutera avec les arguments spécifiés dans la classe de visite

4: injectez-le dans la classe que vous souhaitez tester

5: SUT injecte le résultat de l'opération au visiteur

6: appliquer vos conditions de test à l'attribut de résultat du visiteur

5
sunwukung

Vous pouvez en effet utiliser __call () de manière générique pour accéder à des méthodes protégées. Pour pouvoir tester cette classe

class Example {
    protected function getMessage() {
        return 'hello';
    }
}

vous créez une sous-classe dans ExampleTest.php:

class ExampleExposed extends Example {
    public function __call($method, array $args = array()) {
        if (!method_exists($this, $method))
            throw new BadMethodCallException("method '$method' does not exist");
        return call_user_func_array(array($this, $method), $args);
    }
}

Notez que la méthode __call () ne fait aucune référence à la classe. Vous pouvez donc copier ce qui précède pour chaque classe avec les méthodes protégées que vous souhaitez tester et modifier simplement la déclaration de la classe. Vous pourrez peut-être placer cette fonction dans une classe de base commune, mais je ne l'ai pas essayée.

Désormais, le scénario de test lui-même ne diffère que par l'endroit où vous construisez l'objet à tester, en effectuant la permutation dans ExampleExposed for Example.

class ExampleTest extends PHPUnit_Framework_TestCase {
    function testGetMessage() {
        $fixture = new ExampleExposed();
        self::assertEquals('hello', $fixture->getMessage());
    }
}

Je pense que PHP 5.3 vous permet d’utiliser la réflexion pour modifier directement l’accessibilité des méthodes, mais je suppose que vous devrez le faire pour chaque méthode individuellement.

5
David Harkness

Je suggère de suivre la solution suivante pour la solution de contournement/idée de "Henrik Paul" :)

Vous connaissez les noms des méthodes privées de votre classe. Par exemple, ils sont comme _add (), _edit (), _delete (), etc.

Par conséquent, lorsque vous voulez le tester sous l’unité de test unitaire, appelez simplement les méthodes privées en préfixant et/ou en ajoutant un suffixe à commun Word (par exemple _addPhpunit) afin que la méthode __call () soit appelée (depuis méthode _addPhpunit () n'existe pas) de la classe propriétaire, vous venez de mettre le code nécessaire dans la méthode __call () pour supprimer Word/s avec suffixe/préfixe (Phpunit), puis pour appeler cette méthode privée déduite à partir de là. C'est un autre bon usage des méthodes magiques.

Essaye le.

2
Anirudh Zala