87 votes

Méthode privée fantaisie avec PHPUnit

J'ai une question sur l'utilisation de PHPUnit pour simuler une méthode privée dans une classe. Laissez-moi vous présenter un exemple :

class A {
  public function b() { 
    // some code
    $this->c(); 
    // some more code
  }

  private function c(){ 
    // some code
  }
}

Comment puis-je stub le résultat de la méthode privée pour tester la un peu plus de code une partie de la fonction publique.

Résolu : lecture partielle aquí

105voto

edorian Points 22780

En général, on ne teste pas ou on ne simule pas directement les méthodes privées et protégées.

Ce que vous voulez tester, c'est le public API de votre classe. Tout le reste est un détail d'implémentation pour votre classe et ne devrait pas "casser" vos tests si vous le changez.

Cela vous aide également lorsque vous constatez que vous "ne pouvez pas obtenir une couverture de code à 100 %", car votre classe contient peut-être du code que vous ne pouvez pas exécuter en appelant l'API publique.


Vous ne voulez généralement pas faire ça

Mais si votre classe ressemble à ça :

class a {

    public function b() {
        return 5 + $this->c();
    }

    private function c() {
        return mt_rand(1,3);
    }
}

je peux comprendre la nécessité de vouloir faire un simulacre de c() puisque la fonction "random" est un état global et que vous ne pouvez pas le tester.

La solution "propre?/verbeuse?/surcompliquée-peut-être?/j'aime-autant-que-comme-c'est-habituel

class a {

    public function __construct(RandomGenerator $foo) {
        $this->foo = $foo;
    }

    public function b() {
        return 5 + $this->c();
    }

    private function c() {
        return $this->foo->rand(1,3);
    }
}

maintenant il n'y a plus besoin de simuler "c()" puisqu'il ne contient pas de globales et que vous pouvez tester gentiment.


Si vous ne voulez pas ou ne pouvez pas supprimer l'état global de votre fonction privée (mauvaise chose, mauvaise réalité ou votre définition de mauvais peut être différente), vous pouvez puede test contre la maquette.

// maybe set the function protected for this to work
$testMe = $this->getMock("a", array("c"));
$testMe->expects($this->once())->method("c")->will($this->returnValue(123123));

et exécutez vos tests contre ce simulacre puisque la seule fonction que vous retirez ou simulez est "c()".


Pour citer le livre "Pragmatic Unit Testing" :

"En général, vous ne voulez pas rompre l'encapsulation pour le plaisir de tester (ou comme maman le disait, "n'exposez pas vos parties intimes !"). La plupart du temps, vous devriez être en mesure de tester une classe en exerçant ses méthodes publiques. Si une fonctionnalité importante est cachée derrière un accès privé ou protégé, cela peut être un signe d'alerte qu'il y a une autre classe à l'intérieur qui se bat pour sortir."


Un peu plus : Why you don't want test private methods.

1 votes

Ce n'est pas vrai, dans mon exemple, j'ai besoin de tester le second "un peu de code..." mais ce résultat peut être modifié par le résultat précédent de la fonction privée que je ne peux pas simuler, voici l'exemple : public function b() { // un peu de code if($this->c() == 0) // faire quelque chose ; else // faire quelque chose d'autre // un peu de code }

1 votes

@dyoser : Si vous avez vraiment besoin de simuler une méthode privée, vous suggérez implicitement que le résultat peut changer d'une manière que vous ne pouvez pas contrôler via les méthodes accessibles. Ou : Si le test-case (blackbox- ou greybox-tests) a besoin de savoir quelque chose sur la structure interne de l'unité à tester, quelque chose ne va pas dans l'unité.

0 votes

@dyoser Bien sûr, il peut être modifié par ce que fait la fonction privée, c'est pourquoi vous avez généralement besoin de plus d'un cas de test. Chaque effet possible de la fonction c() sera déclenché par le passage de certains paramètres à votre classe, donc si vous passez les bons éléments, c() fera aussi les bons éléments. Quoi qu'il en soit, je vais construire un petit exemple où vous pourriez vouloir simuler c().

28voto

David Harkness Points 16674

Vous pouvez utiliser réflexion y setAccessible() dans vos tests pour vous permettre de définir l'état interne de votre objet de manière à ce qu'il renvoie ce que vous voulez de la méthode privée. Vous devez être en PHP 5.3.2.

$fixture = new MyClass(...);
$reflector = new ReflectionProperty('MyClass', 'myPrivateProperty');
$reflector->setAccessible(true);
$reflector->setValue($fixture, 'value');
// test $fixture ...

4 votes

Non, vous utilisez la réflexion sur une propriété mais dans l'exemple vous utilisez une METHODE privée. Juste pour clarifier, je veux simuler (mock) l'exécution de la méthode privée pour obtenir le résultat que je veux (ou juste un résultat simulé). Je n'ai pas besoin de le tester.

1 votes

@dyoser - Si vous ne voulez pas modifier la classe testée, vous devez modifier son état de sorte que la méthode privée fonctionne comme vous le souhaitez. Puisque la méthode publique peut appeler la méthode privée, et que vous ne pouvez pas surcharger la méthode privée, vous ne pouvez pas la simuler.

1 votes

A résolu un problème différent pour moi, mais fonctionne comme un charme.

27voto

doctore Points 1140

Vous pouvez tester les méthodes privées mais vous ne pouvez pas simuler (mock) l'exécution de ces méthodes.

En outre, la réflexion ne permet pas de convertir une méthode privée en une méthode protégée ou publique. setAccessible permet uniquement d'invoquer la méthode originale.

Alternativement, vous pouvez utiliser runkit pour renommer les méthodes privées et inclure une "nouvelle implémentation". Toutefois, ces fonctionnalités sont expérimentales et leur utilisation n'est pas recommandée.

0 votes

Qu'en est-il de l'extension de la classe A et de l'implémentation de la nouvelle méthode c ?

0 votes

@PetrPeller - Depuis c est privée, elle ne peut pas être surchargée dans les sous-classes.

10voto

Mark McEver Points 31

Voici une variante des autres réponses qui peuvent être utilisées pour faire de tels appels sur une seule ligne :

public function callPrivateMethod($object, $methodName)
{
    $reflectionClass = new \ReflectionClass($object);
    $reflectionMethod = $reflectionClass->getMethod($methodName);
    $reflectionMethod->setAccessible(true);

    $params = array_slice(func_get_args(), 2); //get all the parameters after $methodName
    return $reflectionMethod->invokeArgs($object, $params);
}

0 votes

Merci pour cet extrait. Il me permet d'économiser beaucoup de travail !

2 votes

La question porte sur la manière de simuler l'appel d'une méthode privée, et non sur la manière d'appeler directement une méthode privée dans un test unitaire.

10voto

Asaph Points 56989

Une option serait de faire c() protected au lieu de private et ensuite sous-classez et surchargez c() . Puis testez avec votre sous-classe. Une autre option serait de remanier c() dans une classe différente que vous pouvez injecter dans A (c'est ce qu'on appelle l'injection de dépendances). Et ensuite, injectez une instance de test avec une implémentation fantaisie de c() dans votre test unitaire.

0 votes

Bien sûr, mais cela signifie que je dois réécrire la classe originale, ce n'est pas une bonne implémentation pour tester la fonctionnalité.

6 votes

@dyoser : Oui, c'est une chose nécessaire. Votre implémentation originale est quelque peu non testable et doit être un peu remaniée pour être testée. N'ayez pas peur de le faire. Michael Feathers parle de cette question dans son excellent livre intitulé Travailler efficacement avec le code hérité .

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