web-dev-qa-db-fra.com

Mock méthode privée avec PHPUnit

J'ai une question sur l'utilisation de PHPUnit pour simuler une méthode privée à l'intérieur d'une classe. Permettez-moi de 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 bloquer le résultat de la méthode privée pour tester la partie un peu plus de code de la fonction publique.

Résolu en lisant partiellement ici

65
Tony

Habituellement, vous ne testez pas ou ne vous moquez pas directement des méthodes privées et protégées.

Ce que vous voulez tester, c'est l'API publique 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 remarquez que vous "ne pouvez pas obtenir une couverture de code à 100%" car vous pouvez avoir du code dans votre classe que vous ne pouvez pas exécuter en appelant l'API publique.


Vous ne voulez généralement pas faire cela

Mais si votre classe ressemble à ceci:

class a {

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

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

je peux voir la nécessité de vouloir se moquer de c() puisque la fonction "aléatoire" est un état global et vous ne pouvez pas le tester.

La solution "propre?/Verbeux?/Trop compliqué-peut-être?/I-comme-il-habituellement"

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'est plus nécessaire de se moquer de "c ()" car il ne contient pas de globales et vous pouvez bien tester.


Si vous ne voulez pas faire 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) que vous pouvez 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 lancez vos tests contre cette maquette puisque la seule fonction que vous retirez/maquette est "c ()".


Pour citer le livre "Pragmatic Unit Testing":

"En général, vous ne voulez pas rompre l'encapsulation pour le test (ou comme maman disait," n'exposez pas vos soldats! "). La plupart du temps, vous devriez pouvoir tester une classe en exerçant ses méthodes publiques. S'il y a des fonctionnalités importantes qui sont cachées derrière un accès privé ou protégé, cela pourrait être un signe avant-coureur qu'une autre classe a du mal à sortir. "


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

81
edorian

Vous pouvez tester des méthodes privées mais vous ne pouvez pas simuler (simuler) le fonctionnement de ces méthodes.

De plus, la réflexion ne vous permet pas de convertir une méthode privée en méthode protégée ou publique. setAccessible vous permet uniquement d'appeler la méthode d'origine.

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

25
doctore

Vous pouvez utiliser réflexion et 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 veulent de la méthode privée. Vous devrez être sur PHP 5.3.2.

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

Vous pouvez obtenir une maquette de la méthode protégée, donc si vous pouvez convertir C en protégé, ce code vous aidera.

 $mock = $this->getMockBuilder('A')
                  ->disableOriginalConstructor()
                  ->setMethods(array('C'))
                  ->getMock();

    $response = $mock->B();

Cela fonctionnera certainement, cela a fonctionné pour moi. Ensuite, pour couvrir la méthode protégée C, vous pouvez utiliser des classes de réflexion.

13
Archit Rastogi

En supposant que vous devez tester $ myClass-> privateMethodX ($ arg1, $ arg2), vous pouvez le faire avec réflexion:

$class = new ReflectionClass ($myClass);
$method = $class->getMethod ('privateMethodX');
$method->setAccessible(true);
$output = $method->invoke ($myClass, $arg1, $arg2);
10
Edson Medina

Voici une variante des autres réponses qui peuvent être utilisées pour effectuer 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);
}
9
Mark McEver

Je suis venu avec cette classe à usage général pour mon cas:

/**
 * @author Torge Kummerow
 */
class Liberator {
    private $originalObject;
    private $class;

    public function __construct($originalObject) {
        $this->originalObject = $originalObject;
        $this->class = new ReflectionClass($originalObject);
    }

    public function __get($name) {
        $property = $this->class->getProperty($name);
        $property->setAccessible(true);
        return $property->getValue($this->originalObject);
    }

    public function __set($name, $value) {
        $property = $this->class->getProperty($name);            
        $property->setAccessible(true);
        $property->setValue($this->originalObject, $value);
    }

    public function __call($name, $args) {
        $method = $this->class->getMethod($name);
        $method->setAccessible(true);
        return $method->invokeArgs($this->originalObject, $args);
    }
}

Avec cette classe, vous pouvez désormais libérer facilement et de manière transparente toutes les fonctions/champs privés sur n'importe quel objet.

$myObject = new Liberator(new MyObject());
/* @var $myObject MyObject */  //Usefull for code completion in some IDEs

//Writing to a private field
$myObject->somePrivateField = "testData";

//Reading a private field
echo $myObject->somePrivateField;

//calling a private function
$result = $myObject->somePrivateFunction($arg1, $arg2);

Si les performances sont importantes, elles peuvent être améliorées en mettant en cache les propriétés/méthodes appelées dans la classe Liberator.

6
Torge

Une option consisterait à créer c()protected au lieu de private, puis à sous-classer et à remplacer c(). Testez ensuite avec votre sous-classe. Une autre option serait de refactoriser c() dans une classe différente que vous pouvez injecter dans A (c'est ce qu'on appelle l'injection de dépendance). Et puis injectez une instance de test avec une implémentation simulée de c() dans votre test unitaire.

5
Asaph

Une autre solution consiste à remplacer votre méthode privée par une méthode protégée, puis à vous moquer.

$myMockObject = $this->getMockBuilder('MyMockClass')
        ->setMethods(array('__construct'))
        ->setConstructorArgs(array("someValue", 5))
        ->setMethods(array('myProtectedMethod'))
        ->getMock();

$response = $myMockObject->myPublicMethod();

myPublicMethod appelle myProtectedMethod. Malheureusement, nous ne pouvons pas le faire avec des méthodes privées car setMethods ne peut pas trouver une méthode privée alors qu'il peut trouver une méthode protégée

2
Thellimist

Vous pouvez utiliser des classes anonymes en utilisant PHP 7.

$mock = new class Concrete {
    private function bob():void
    {
    }
};

Dans les versions antérieures de PHP, vous pouvez créer une classe de test étendant la classe de base.

0
jgmjgm