it-swarm.com.de

Private Methode mit PHPUnit verspotten

Ich habe eine Frage zur Verwendung von PHPUnit, um eine private Methode innerhalb einer Klasse zu verspotten. Lassen Sie mich mit einem Beispiel vorstellen:

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

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

Wie kann ich das Ergebnis der privaten Methode abkoppeln, um den weiteren Code-Teil der öffentlichen Funktion zu testen?.

Gelöst teilweise Lesen hier

65
Tony

Normalerweise testen oder verspotten Sie die privaten und geschützten Methoden nicht direkt.

Was Sie testen möchten, ist die öffentliche API Ihrer Klasse. Alles andere ist ein Implementierungsdetail für Ihre Klasse und sollte Ihre Tests nicht "unterbrechen", wenn Sie sie ändern.

Das hilft Ihnen auch, wenn Sie feststellen, dass Sie "keine 100% ige Codeabdeckung erhalten", weil Ihre Klasse möglicherweise Code enthält, den Sie nicht durch Aufrufen der öffentlichen API ausführen können.


Normalerweise möchten Sie dies nicht tun

Aber wenn deine Klasse so aussieht:

class a {

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

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

ich kann die Notwendigkeit sehen zu wollen, zu verspotten c() da die "zufällige" Funktion globaler Zustand ist und Sie das nicht testen können.

Die "saubere?/Ausführliche?/Überkomplizierte-vielleicht?/Ich-mag-es-normalerweise" Lösung

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

jetzt muss "c ()" nicht mehr verspottet werden, da es keine globalen Elemente enthält und Sie gut testen können.


Wenn Sie den globalen Status nicht aus Ihrer privaten Funktion entfernen möchten oder können (schlechte Sache, schlechte Realität oder Ihre Definition von schlecht könnte anders sein), können Sie Test gegen den Schein.

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

und führen Sie Ihre Tests mit diesem Mock durch, da die einzige Funktion, die Sie ausführen/mock ist "c ()".


So zitieren Sie das Buch "Pragmatic Unit Testing":

"Im Allgemeinen möchten Sie keine Kapselung zum Testen aufheben (oder, wie Mama immer sagte," machen Sie Ihre Privaten nicht sichtbar! "). Meistens sollten Sie in der Lage sein, eine Klasse zu testen Wenn sich hinter einem privaten oder geschützten Zugang eine bedeutende Funktionalität verbirgt, könnte dies ein Warnsignal dafür sein, dass eine andere Klasse da drin ist, die Schwierigkeiten hat, herauszukommen. "


Einige mehr: Why you don't want test private methods.

81
edorian

Sie können private Methoden testen aber Sie können die Ausführung dieser Methoden nicht simulieren (verspotten).

Darüber hinaus können Sie mit der Reflektion keine private Methode in eine geschützte oder öffentliche Methode konvertieren. setAccessible erlaubt nur das Aufrufen der ursprünglichen Methode.

Alternativ können Sie runkit verwenden, um die privaten Methoden umzubenennen und eine "neue Implementierung" hinzuzufügen. Diese Funktionen sind jedoch experimentell und ihre Verwendung wird nicht empfohlen.

25
doctore

Sie können Reflection und setAccessible() in Ihren Tests verwenden, um den internen Status Ihres Objekts so festzulegen, dass es das zurückgibt, was Sie erhalten will von der privaten Methode. Sie müssen PHP 5.3.2.

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

Sie können sich über eine geschützte Methode lustig machen. Wenn Sie also C in eine geschützte konvertieren können, hilft dieser Code.

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

    $response = $mock->B();

Das wird auf jeden Fall funktionieren. Es hat bei mir funktioniert. Dann können Sie zur Abdeckung der geschützten Methode C Reflexionsklassen verwenden.

13
Archit Rastogi

Angenommen, Sie müssen $ myClass-> privateMethodX ($ arg1, $ arg2) testen, dann können Sie dies mit Reflection tun:

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

Hier ist eine Variation der anderen Antworten, mit denen solche Anrufe in einer Zeile getätigt werden können:

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

Ich habe mir für meinen Fall diese Allzweckklasse ausgedacht:

/**
 * @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);
    }
}

Mit dieser Klasse können Sie jetzt einfach und transparent alle privaten Funktionen/Felder für jedes Objekt freigeben.

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

Wenn die Leistung wichtig ist, kann sie durch Zwischenspeichern der in der Liberator-Klasse aufgerufenen Eigenschaften/Methoden verbessert werden.

6
Torge

Eine Möglichkeit wäre, c()protected anstelle von private zu setzen und dann c() zu unterteilen und zu überschreiben. Dann teste mit deiner Unterklasse. Eine andere Möglichkeit wäre, c() in eine andere Klasse umzuwandeln, die Sie in A einfügen können (dies wird als Abhängigkeitsinjektion bezeichnet). Fügen Sie dann eine Testinstanz mit einer Scheinimplementierung von c() in Ihren Komponententest ein.

5
Asaph

Eine alternative Lösung besteht darin, Ihre private Methode in eine geschützte Methode zu ändern und dann zu verspotten.

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

$response = $myMockObject->myPublicMethod();

dabei ruft myPublicMethodmyProtectedMethod auf. Leider können wir dies nicht mit privaten Methoden tun, da setMethods keine private Methode finden kann, da es eine geschützte Methode finden kann

2
Thellimist

Sie können anonyme Klassen mit PHP 7.

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

In früheren Versionen von PHP können Sie eine Testklasse erstellen, die die Basisklasse erweitert.

0
jgmjgm