Meilleures pratiques pour tester les méthodes protégées avec PHPUnit

287

J'ai trouvé la discussion sur Do you test private method informative.

J'ai décidé que, dans certaines classes, je veux avoir des méthodes protégées, mais testez-les. Certaines de ces méthodes sont statiques et courtes. Étant donné que la plupart des méthodes publiques les utilisent, je pourrai probablement 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 de méthode comme conseillé dans une réponse semble être exagéré pour cela.
  • Commencez avec des méthodes publiques et lorsque la couverture de code est donnée par des tests de niveau supérieur, protégez-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 l'ai pas approfondi. PHP ne permet pas cela via la réflexion .

GrGr
la source
Deux questions: 1. pourquoi devriez-vous prendre la peine de tester les fonctionnalités que votre classe n'expose pas? 2. Si vous devez le tester, pourquoi est-il privé?
nad2000
2
Peut-être qu'il veut tester si une propriété privée est définie correctement et la seule façon de tester en utilisant uniquement la fonction setter est de rendre la propriété privée publique et de vérifier les données
AntonioCS
4
Et c'est donc un style de discussion et donc pas constructif. Encore une fois :)
mlvljr
72
Vous pouvez l'appeler contre les règles du site, mais le qualifier de "pas constructif" est ... c'est insultant.
Andy V
1
@Visser, c'est s'insulter;)
Pacerier

Réponses:

417

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 définir comme 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(...));
  ...
}
uckelman
la source
27
Pour citer le lien vers le blog des sebastiens: "Donc: ce n'est pas parce que le test d'attributs et de méthodes protégés et privés ne signifie pas que c'est" une bonne chose "." - Juste pour garder cela à l'esprit
edorian
10
Je contesterais cela. Si vous n'avez pas besoin que vos méthodes protégées ou privées fonctionnent, ne les testez pas.
uckelman
10
Juste pour clarifier, vous n'avez pas besoin d'utiliser PHPUnit pour que cela fonctionne. Cela fonctionnera également avec SimpleTest ou autre. Il n'y a rien dans la réponse qui dépend de PHPUnit.
Ian Dunn
84
Vous ne devez pas tester directement les membres protégés / privés. Ils appartiennent à l'implémentation interne de la classe et ne doivent pas être couplés au test. Cela rend le refactoring impossible et finalement vous ne testez pas ce qui doit être testé. Vous devez les tester indirectement à l'aide de méthodes publiques. Si vous trouvez cela difficile, presque sûr qu'il y a un problème avec la composition de la classe et que vous devez la séparer en classes plus petites. Gardez à l'esprit que votre classe doit être une boîte noire pour votre test - vous jetez quelque chose et vous récupérez quelque chose, et c'est tout!
gphilip
24
@gphilip Pour moi, la protectedméthode fait également partie de l'API publique car toute classe tierce peut l'étendre et l'utiliser sans magie. Je pense donc que seules les privateméthodes entrent dans la catégorie des méthodes à ne pas tester directement. protectedet publicdoit être testé directement.
Filip Halaxa
48

Vous semblez déjà au courant, mais je vais tout de même le répéter; 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 et 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 comme fournissant une interface pour la sous-classe. Donc, ici, vous devrez tester la méthode protégée (mais jamais une méthode privée ). 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. Lors du test de code, il est généralement beaucoup plus facile de gérer le code qui utilise ce modèle, vous pouvez donc envisager cette option.

troelskn
la source
2
Vous pouvez simplement implémenter directement stuff () en tant que public et retourner parent :: stuff (). Voir ma réponse. Il semble que je lis les choses trop rapidement aujourd'hui.
Michael Johnson
Vous avez raison; Il est valide de changer une méthode protégée en une méthode publique.
troelskn
Le code suggère donc ma troisième option et "Notez que vous pouvez toujours remplacer l'héritage par la composition". va dans le sens de ma première option ou refactoring.com/catalog/replaceInheritanceWithDelegation.html
GrGr
34
Je ne suis pas d'accord pour dire que c'est un mauvais signe. Faisons une différence entre TDD et Unit Testing. Les tests unitaires devraient tester les méthodes privées imo, car ce sont des unités et bénéficieraient de la même manière que les méthodes unitaires publiques bénéficient des tests unitaires.
koen
36
Les méthodes protégées font partie de l'interface d'une classe, ce ne sont pas simplement des détails d'implémentation. L'intérêt des membres protégés est que les sous-classes (utilisateurs à part entière) puissent utiliser ces méthodes protégées à l'intérieur des classes d'exstions. Ceux-ci doivent clairement être testés.
BT
40

teastburn a la bonne approche. Encore plus simple est d'appeler directement la méthode et de renvoyer 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 l'appeler simplement dans vos tests en:

$returnVal = PHPUnitUtil::callMethod(
                $this->object,
                '_nameOfProtectedMethod', 
                array($arg1, $arg2)
             );
robert.egginton
la source
1
Ceci est un excellent exemple, merci. La méthode devrait être publique au lieu d'être protégée, n'est-ce pas?
valk
Bon point. J'utilise en fait cette méthode dans ma classe de base à partir de laquelle j'étends mes classes de test, auquel cas cela a du sens. Le nom de la classe serait faux ici cependant.
robert.egginton
J'ai fait exactement le même morceau de code basé sur teastburn xD
Nebulosar
23

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

Cette version change 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 d'extension PHPUnit_Framework_TestCase (ou, je suppose, globalement à votre fichier PHPUnitUtil).

Puisque MyClass est de toute façon instancié et 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 d'alias getProtectedMethod () pour être explicite sur ce qui est attendu, mais c'est à vous de décider.

À votre santé!

brûlures d'estomac
la source
+1 pour l'utilisation de l'API Reflection Class.
Bill Ortell
10

Je pense que troelskn est proche. Je ferais cela à la place:

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

Ensuite, implémentez quelque chose comme ceci:

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

Vous exécutez ensuite vos tests par rapport à 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 propose déjà un tel mécanisme (même si je n'ai pas vérifié).

Michael Johnson
la source
Hé ... il semble que je dis, utilisez votre troisième option :)
Michael Johnson
2
Oui, c'est exactement ma troisième option. Je suis à peu près sûr que PHPUnit n'offre pas un tel mécanisme.
GrGr
Cela ne fonctionnera pas, vous ne pouvez pas remplacer une fonction protégée par une fonction publique du même nom.
Koen.
Je me trompe peut-être, mais je ne pense pas que cette approche puisse fonctionner. PHPUnit (pour autant que je l'ai jamais utilisé) nécessite que votre classe de test étende une autre classe qui fournit la fonctionnalité de test réelle. À moins qu'il n'existe un moyen de contourner ce problème, je ne suis pas sûr de pouvoir voir comment cette réponse peut être utilisée. phpunit.de/manual/current/en/…
Cypher
Pour info cet onl fonctionne pour les méthodes protégées , pas pour les méthodes privées
Sliq
5

Je vais jeter mon chapeau dans le ring ici:

J'ai utilisé le hack __call avec des degrés de réussite mitigés. L'alternative que j'ai trouvée était d'utiliser le modèle Visitor:

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

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

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

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

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

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

sunwukung
la source
1
+1 pour une solution intéressante
jsh
5

Vous pouvez en effet utiliser __call () de manière générique pour accéder aux 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 référence en aucune façon 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 simplement modifier la déclaration de classe. Vous pourrez peut-être placer cette fonction dans une classe de base commune, mais je ne l'ai pas essayée.

Maintenant, le cas de test lui-même ne diffère que par l'endroit où vous construisez l'objet à tester, en échangeant dans ExampleExposed for Example.

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

Je crois 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.

David Harkness
la source
1
L'implémentation de __call () fonctionne très bien! J'ai essayé de voter, mais je n'ai annulé mon vote qu'après avoir testé cette méthode et maintenant je ne suis pas autorisé à voter en raison d'une limite de temps dans SO.
Adam Franco
La call_user_method_array()fonction est obsolète à partir de PHP 4.1.0 ... utilisez call_user_func_array(array($this, $method), $args)plutôt. Notez que si vous utilisez PHP 5.3.2+, vous pouvez utiliser Reflection pour accéder aux méthodes et attributs protégés / privés
nuqqsa
@nuqqsa - Merci, j'ai mis à jour ma réponse. J'ai depuis écrit un Accessiblepackage générique qui utilise la réflexion pour permettre aux tests d'accéder aux propriétés et méthodes privées / protégées des classes et des objets.
David Harkness
Ce code ne fonctionne pas pour moi sur PHP 5.2.7 - la méthode __call n'est pas invoquée pour les méthodes définies par la classe de base. Je ne le trouve pas documenté, mais je suppose que ce comportement a été modifié en PHP 5.3 (où j'ai confirmé que cela fonctionne).
Russell Davis
@Russell - __call()n'est invoqué que si l'appelant n'a pas accès à la méthode. Puisque la classe et ses sous-classes ont accès aux méthodes protégées, les appels à celles-ci ne passeront pas __call(). Pouvez-vous publier votre code qui ne fonctionne pas dans 5.2.7 dans une nouvelle question? J'ai utilisé ce qui précède dans 5.2 et je suis passé à l'utilisation de la réflexion avec 5.3.2.
David Harkness
2

Je suggère la solution de contournement suivante pour la solution / 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'aspect des tests unitaires, il suffit d'appeler des méthodes privées en préfixant et / ou en suffixant un mot commun (par exemple _addPhpunit) de sorte que lorsque la méthode __call () est appelée (puisque la méthode _addPhpunit () ne le fait pas existe) de la classe propriétaire, vous venez de mettre le code nécessaire dans la méthode __call () pour supprimer les mots / s préfixés / suffixés (Phpunit), puis pour appeler cette méthode privée déduite à partir de là. Ceci est une autre bonne utilisation des méthodes magiques.

Essaye le.

Anirudh Zala
la source