Best Practices zum Testen geschützter Methoden mit PHPUnit [geschlossen]

287

Ich fand die Diskussion über Testen Sie private Methoden informativ.

Ich habe beschlossen, dass ich in einigen Klassen geschützte Methoden haben möchte, aber sie testen. Einige dieser Methoden sind statisch und kurz. Da die meisten öffentlichen Methoden sie verwenden, kann ich die Tests wahrscheinlich später sicher entfernen. Aber um mit einem TDD-Ansatz zu beginnen und das Debuggen zu vermeiden, möchte ich sie wirklich testen.

Ich dachte an Folgendes:

  • Das in einer Antwort empfohlene Methodenobjekt scheint dafür übertrieben zu sein.
  • Beginnen Sie mit öffentlichen Methoden. Wenn die Codeabdeckung durch Tests höherer Ebene gegeben ist, schalten Sie sie geschützt aus und entfernen Sie die Tests.
  • Erben Sie eine Klasse mit einer testbaren Schnittstelle, die geschützte Methoden öffentlich macht

Welches ist die beste Vorgehensweise? Gibt es noch etwas?

Es scheint, dass JUnit geschützte Methoden automatisch ändert, um öffentlich zu sein, aber ich habe es mir nicht genauer angesehen. PHP erlaubt dies nicht durch Reflexion .

GrGr
quelle
Zwei Fragen: 1. Warum sollten Sie sich die Mühe machen, Funktionen zu testen, die Ihre Klasse nicht verfügbar macht? 2. Wenn Sie es testen sollten, warum ist es privat?
nad2000
2
Vielleicht möchte er testen, ob eine private Eigenschaft korrekt eingestellt ist, und die einzige Möglichkeit, nur mit der Setter-Funktion zu testen, besteht darin, die private Eigenschaft öffentlich zu machen und die Daten zu überprüfen
AntonioCS
4
Das ist also diskussionsartig und daher nicht konstruktiv. Wieder :)
mlvljr
72
Sie können es gegen die Regeln der Site aufrufen, aber es einfach "nicht konstruktiv" zu nennen, ist ... beleidigend.
Andy V
1
@ Visser, es beleidigt sich selbst;)
Pacerier

Antworten:

416

Wenn Sie PHP5 (> = 5.3.2) mit PHPUnit verwenden, können Sie Ihre privaten und geschützten Methoden testen, indem Sie Reflection verwenden, um sie vor dem Ausführen Ihrer Tests als öffentlich festzulegen:

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
quelle
27
Um den Link zum Blog von Sebastian zu zitieren: "Also: Nur weil das Testen von geschützten und privaten Attributen und Methoden möglich ist, heißt das nicht, dass dies eine" gute Sache "ist." - Nur um das zu bedenken
edorian
10
Ich würde das bestreiten. Wenn Sie Ihre geschützten oder privaten Methoden nicht benötigen, testen Sie sie nicht.
uckelman
10
Zur Verdeutlichung müssen Sie PHPUnit nicht verwenden, damit dies funktioniert. Es funktioniert auch mit SimpleTest oder was auch immer. Es gibt nichts an der Antwort, das von PHPUnit abhängt.
Ian Dunn
84
Sie sollten geschützte / private Mitglieder nicht direkt testen. Sie gehören zur internen Implementierung der Klasse und sollten nicht mit dem Test gekoppelt werden. Dies macht ein Refactoring unmöglich und schließlich testen Sie nicht, was getestet werden muss. Sie müssen sie indirekt mit öffentlichen Methoden testen. Wenn Sie dies schwierig finden, ist es fast sicher, dass es ein Problem mit der Zusammensetzung der Klasse gibt, und Sie müssen es in kleinere Klassen aufteilen. Denken Sie daran, dass Ihre Klasse eine Black Box für Ihren Test sein sollte - Sie werfen etwas ein und Sie bekommen etwas zurück, und das ist alles!
Gphilip
24
@gphilip Für mich ist die protectedMethode auch Teil der öffentlichen API, da jede Klasse von Drittanbietern sie erweitern und ohne Magie verwenden kann. Ich denke also, dass nur privateMethoden in die Kategorie der Methoden fallen, die nicht direkt getestet werden sollen. protectedund publicsollte direkt getestet werden.
Filip Halaxa
48

Sie scheinen es bereits zu wissen, aber ich werde es trotzdem wiederholen. Es ist ein schlechtes Zeichen, wenn Sie geschützte Methoden testen müssen. Das Ziel eines Komponententests besteht darin, die Schnittstelle einer Klasse zu testen, und geschützte Methoden sind Implementierungsdetails. Es gibt jedoch Fälle, in denen dies sinnvoll ist. Wenn Sie die Vererbung verwenden, sehen Sie eine Oberklasse als Schnittstelle für die Unterklasse. Hier müssten Sie also die geschützte Methode testen (aber niemals eine private ). Die Lösung hierfür besteht darin, zu Testzwecken eine Unterklasse zu erstellen und damit die Methoden verfügbar zu machen. Z.B.:

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

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

Beachten Sie, dass Sie die Vererbung jederzeit durch Komposition ersetzen können. Beim Testen von Code ist es normalerweise viel einfacher, mit Code umzugehen, der dieses Muster verwendet. Daher sollten Sie diese Option in Betracht ziehen.

troelskn
quelle
2
Sie können stuff () einfach direkt als public implementieren und parent :: stuff () zurückgeben. Siehe meine Antwort. Es scheint, dass ich heute zu schnell lese.
Michael Johnson
Du hast recht; Es ist gültig, eine geschützte Methode in eine öffentliche zu ändern.
troelskn
Der Code schlägt also meine dritte Option vor und "Beachten Sie, dass Sie die Vererbung immer durch Komposition ersetzen können." geht in die Richtung meiner ersten Option oder refactoring.com/catalog/replaceInheritanceWithDelegation.html
grgr
34
Ich stimme nicht zu, dass es ein schlechtes Zeichen ist. Machen wir einen Unterschied zwischen TDD und Unit Testing. Unit-Tests sollten private Methoden imo testen, da dies Einheiten sind und genauso profitieren würden wie Unit-Tests. Öffentliche Methoden profitieren von Unit-Tests.
Koen
36
Geschützte Methoden sind Teil der Schnittstelle einer Klasse, sie sind nicht nur Implementierungsdetails. Der springende Punkt bei geschützten Mitgliedern ist, dass Unterklassen (eigenständige Benutzer) diese geschützten Methoden in Klassenexstionen verwenden können. Diese müssen eindeutig getestet werden.
BT
40

Teastburn hat den richtigen Ansatz. Noch einfacher ist es, die Methode direkt aufzurufen und die Antwort zurückzugeben:

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

Sie können dies einfach in Ihren Tests aufrufen, indem Sie:

$returnVal = PHPUnitUtil::callMethod(
                $this->object,
                '_nameOfProtectedMethod', 
                array($arg1, $arg2)
             );
robert.egginton
quelle
1
Dies ist ein großartiges Beispiel, danke. Die Methode sollte öffentlich statt geschützt sein, nicht wahr?
Valk
Guter Punkt. Ich verwende diese Methode tatsächlich in meiner Basisklasse, von der aus ich meine Testklassen erweitere. In diesem Fall ist dies sinnvoll. Der Name der Klasse wäre hier allerdings falsch.
robert.egginton
Ich habe genau den gleichen Code basierend auf Teastburn xD
Nebulosar
23

Ich möchte eine kleine Variation von getMethod () vorschlagen, die in uckelmans Antwort definiert ist .

Diese Version ändert getMethod (), indem fest codierte Werte entfernt und die Verwendung ein wenig vereinfacht werden. Ich empfehle, es Ihrer PHPUnitUtil-Klasse wie im folgenden Beispiel oder Ihrer PHPUnit_Framework_TestCase-Erweiterungsklasse (oder vermutlich global Ihrer PHPUnitUtil-Datei) hinzuzufügen.

Da MyClass sowieso instanziiert wird und ReflectionClass eine Zeichenfolge oder ein Objekt aufnehmen kann ...

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
}

Ich habe auch eine Alias-Funktion getProtectedMethod () erstellt, um explizit anzugeben, was erwartet wird, aber das liegt bei Ihnen.

Prost!

Teastburn
quelle
+1 für die Verwendung der Reflection Class API.
Bill Ortell
10

Ich denke, troelskn ist nah dran. Ich würde dies stattdessen tun:

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

Implementieren Sie dann Folgendes:

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

Anschließend führen Sie Ihre Tests gegen TestClassToTest aus.

Es sollte möglich sein, solche Erweiterungsklassen automatisch zu generieren, indem der Code analysiert wird. Es würde mich nicht wundern, wenn PHPUnit bereits einen solchen Mechanismus anbietet (obwohl ich nicht überprüft habe).

Michael Johnson
quelle
Heh ... es scheint, ich sage, benutze deine dritte Option :)
Michael Johnson
2
Ja, das ist genau meine dritte Option. Ich bin mir ziemlich sicher, dass PHPUnit einen solchen Mechanismus nicht bietet.
GrGr
Dies funktioniert nicht. Sie können eine geschützte Funktion nicht mit einer öffentlichen Funktion mit demselben Namen überschreiben.
Koen.
Ich könnte mich irren, aber ich glaube nicht, dass dieser Ansatz funktionieren kann. PHPUnit (soweit ich es jemals verwendet habe) erfordert, dass Ihre Testklasse eine andere Klasse erweitert, die die eigentliche Testfunktionalität bietet. Ich bin mir nicht sicher, ob ich sehen kann, wie diese Antwort verwendet werden kann, es sei denn, es gibt einen Ausweg. phpunit.de/manual/current/en/…
Cypher
Zu Ihrer Information, dies funktioniert nur für geschützte Methoden, nicht für private
Sliq
5

Ich werde hier meinen Hut in den Ring werfen:

Ich habe den __call-Hack mit gemischtem Erfolg verwendet. Die Alternative, die ich mir ausgedacht habe, war die Verwendung des Besuchermusters:

1: Generieren Sie eine stdClass oder eine benutzerdefinierte Klasse (um den Typ zu erzwingen).

2: Primen Sie das mit der erforderlichen Methode und den erforderlichen Argumenten

3: Stellen Sie sicher, dass Ihr SUT über eine acceptVisitor-Methode verfügt, die die Methode mit den in der besuchenden Klasse angegebenen Argumenten ausführt

4: Injizieren Sie es in die Klasse, die Sie testen möchten

5: SUT injiziert das Ergebnis der Operation in den Besucher

6: Wenden Sie Ihre Testbedingungen auf das Ergebnisattribut des Besuchers an

Sunwukung
quelle
1
+1 für eine interessante Lösung
jsh
5

Sie können __call () generisch verwenden, um auf geschützte Methoden zuzugreifen. Um diese Klasse testen zu können

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

Sie erstellen eine Unterklasse in 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);
    }
}

Beachten Sie, dass die __call () -Methode in keiner Weise auf die Klasse verweist, sodass Sie das Obige für jede Klasse mit geschützten Methoden kopieren können, die Sie testen möchten, und einfach die Klassendeklaration ändern können. Möglicherweise können Sie diese Funktion in eine gemeinsame Basisklasse einfügen, aber ich habe sie nicht ausprobiert.

Jetzt unterscheidet sich der Testfall selbst nur darin, wo Sie das zu testende Objekt erstellen und in ExampleExposed gegen Example austauschen.

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

Ich glaube, mit PHP 5.3 können Sie Reflection verwenden, um die Zugänglichkeit von Methoden direkt zu ändern, aber ich gehe davon aus, dass Sie dies für jede Methode einzeln tun müssen.

David Harkness
quelle
1
Die Implementierung von __call () funktioniert hervorragend! Ich habe versucht, abzustimmen, aber ich habe meine Abstimmung erst aufgehoben, nachdem ich diese Methode getestet habe. Jetzt darf ich aufgrund einer zeitlichen Begrenzung in SO nicht mehr abstimmen.
Adam Franco
Die call_user_method_array()Funktion ist ab PHP 4.1.0 veraltet ... call_user_func_array(array($this, $method), $args)stattdessen verwenden. Beachten Sie, dass Sie, wenn Sie PHP 5.3.2+ verwenden, Reflection verwenden können, um Zugriff auf geschützte / private Methoden und Attribute zu erhalten
nuqqsa
@nuqqsa - Danke, ich habe meine Antwort aktualisiert. Ich habe seitdem ein generisches AccessiblePaket geschrieben, das Reflektion verwendet, um Tests den Zugriff auf private / geschützte Eigenschaften und Methoden von Klassen und Objekten zu ermöglichen.
David Harkness
Dieser Code funktioniert unter PHP 5.2.7 nicht - die __call-Methode wird nicht für Methoden aufgerufen, die von der Basisklasse definiert werden. Ich kann es nicht dokumentiert finden, aber ich vermute, dass dieses Verhalten in PHP 5.3 geändert wurde (wo ich bestätigt habe, dass es funktioniert).
Russell Davis
@Russell - wird __call()nur aufgerufen, wenn der Aufrufer keinen Zugriff auf die Methode hat. Da die Klasse und ihre Unterklassen Zugriff auf die geschützten Methoden haben, werden Aufrufe an sie nicht durchgeführt __call(). Können Sie Ihren Code, der in 5.2.7 nicht funktioniert, in einer neuen Frage veröffentlichen? Ich habe das Obige in 5.2 verwendet und bin erst mit 5.3.2 zur Reflexion übergegangen.
David Harkness
2

Ich schlage vor, die Problemumgehung für die Problemumgehung / Idee von "Henrik Paul" zu befolgen :)

Sie kennen Namen privater Methoden Ihrer Klasse. Zum Beispiel sind sie wie _add (), _edit (), _delete () usw.

Wenn Sie es daher unter dem Aspekt des Unit-Tests testen möchten, rufen Sie einfach private Methoden auf, indem Sie ein allgemeines Wort (z. B. _addPhpunit) voranstellen und / oder anhängen, damit beim Aufrufen der Methode __call () die Methode aufgerufen wird (da die Methode _addPhpunit () dies nicht tut exist) der Eigentümerklasse, geben Sie einfach den erforderlichen Code in die __call () -Methode ein, um vorangestellte / angehängte Wörter (Phpunit) zu entfernen und dann die abgeleitete private Methode von dort aufzurufen. Dies ist eine weitere gute Anwendung magischer Methoden.

Versuch es.

Anirudh Zala
quelle