Zu Beginn eines Tests wird das so genannte Testinventar (Englisch: Test Fixture) vorbereitet. Soll beispielsweise eine Methode getestet werden, so wird ein Objekt der entsprechenden Klasse benötigt. Nach dieser Vorbereitung wird die Aktion ausgelöst, beispielsweise das Aufrufen einer Methode, deren Ergebnisse getestet werden sollen. Schließlich wird überprüft, ob die erwarteten Ergebnisse eingetreten sind.
Manchmal ist das Testinventar komplexer als ein einzelnes einfaches Objekt. Die Menge an Code, die zum Vorbereiten des Testinventars benötigt wird, wächst entsprechend. Wenn wir nicht aufpassen, leidet die Lesbarkeit der Testmethode unter der Vermischung von Code für die Vorbereitung des Testinventars mit dem eigentlichen Testcode. Es ist daher sinnvoll, den Code für die Vorbereitung des Testinventars in eine separate Methode zu verschieben.
Testinventar mit setUp()
Von Anfang an bietet PHPUnit die beiden Schablonenmethoden setUp()
und tearDown()
an, um den Code für die Verwaltung des Testinventars vom eigentlichen Testcode zu trennen:
<?php declare ( strict_types = 1 ) ; |
namespace example ; |
use PHPUnit\Framework\TestCase ; |
final class ExampleTest extends TestCase |
{ |
private ? Example $example ; |
public function testSomething ( ) : void |
{ |
$this -> assertSame ( |
'the-result' , |
$this -> example -> doSomething ( ) |
) ; |
} |
protected function setUp ( ) : void |
{ |
$this -> example = new Example ( |
$this -> createStub ( Collaborator :: class ) |
) ; |
} |
protected function tearDown ( ) : void |
{ |
$this -> example = null ; |
} |
} |
Vor jedem Test ruft PHPUnit die Schablonenmethode setUp()
auf, nach jedem Test tearDown()
. Die Dokumentation von PHPUnit sagt hierzu:
setUp()
undtearDown()
sind in der Theorie schön symmetrisch, aber nicht in der Praxis. In der Praxis müssen SietearDown()
nur implementieren, wenn Sie externe Ressourcen wie Dateien oder Sockets insetUp()
zugewiesen haben. Wenn IhrsetUp()
nur einfache PHP-Objekte erzeugt, können SietearDown()
im Allgemeinen ignorieren.Wenn Sie jedoch viele Objekte in Ihrem
setUp()
erstellen, sollten Sie die Variablen, die diese Objekte enthalten, in IhremtearDown()
mitunset()
zurücksetzen, damit sie früher aufgeräumt werden können. Objekte, die innerhalb vonsetUp()
(oder Testmethoden) erstellt und in Eigenschaften des Testobjekts gespeichert werden, werden erst am Ende des PHP-Prozesses, der PHPUnit ausführt, automatisch aufgeräumt.
Ein Problem der Schablonenmethoden setUp()
und tearDown()
ist, dass sie vor beziehungsweise nach jedem Test der Testklasse aufgerufen werden. Also auch für Tests, die das von diesen Methoden verwaltete Testinventar, im oben gezeigten Beispiel die Eigenschaft $this->example
, nicht verwenden.
Ein weiteres Problem kann auftreten, wenn Vererbung ins Spiel kommt:
<?php declare ( strict_types = 1 ) ; |
namespace example ; |
use PHPUnit\Framework\TestCase ; |
abstract class MyTestCase extends TestCase |
{ |
protected function setUp ( ) : void |
{ |
// ... |
} |
} |
<?php declare ( strict_types = 1 ) ; |
namespace example ; |
use PHPUnit\Framework\TestCase ; |
final class ExampleTest extends MyTestCase |
{ |
protected function setUp ( ) : void |
{ |
// ... |
} |
} |
Wenn wir bei der Implementierung von ExampleTest::setUp()
vergessen, parent::setUp()
aufzurufen, so wird die von MyTestCase
zur Verfügung gestellte Funktionalität nicht funktionieren. Um dieses Risiko zu reduzieren, wurden schon vor langer Zeit die Annotationen @before
und @after
eingeführt. Mit diesen können mehrere Methoden für die Ausführung vor beziehungsweise nach einem Test konfiguriert werden.
Testinventar ohne setUp()
Den eingangs gezeigten Test schreibe ich heute so:
<?php declare ( strict_types = 1 ) ; |
namespace example ; |
use PHPUnit\Framework\TestCase ; |
final class ExampleTest extends TestCase |
{ |
public function testSomething ( ) : void |
{ |
$this -> assertSame ( |
'the-result' , |
$this -> example ( ) -> doSomething ( ) |
) ; |
} |
private function example ( ) : Example |
{ |
return new Example ( |
$this -> createStub ( Collaborator :: class ) |
) ; |
} |
} |
Im Testobjekt wird kein testspezifischer Zustand, beispielsweise in einer Eigenschaft wie $this->example
, gespeichert. Nachdem die Ausführung der Testmethode beendet ist, hält der Test keine Referenz mehr auf das in der Methode example()
erzeugte Objekt. Der Garbage Collector von PHP kann es daher automatisch aufräumen.
Die Methode example()
wird nur von den Tests aufgerufen, die das so erzeugte Objekt benötigen. Darüber hinaus ist sie ein privates Implementierungsdetail der Testklasse, Probleme durch Vererbung sind daher ausgeschlossen.
Einen Typ für den Rückgabewert der Methode example()
konnte ich übrigens schon deklarieren, bevor PHP 7.4 endlich Typdeklarationen für Eigenschaften einführte.
Ich glaube es sind diese drei Gründe, die dazu geführt haben, dass ich unterbewusst die Art und Weise, wie ich in meinen Tests das Testinventar verwalte, geändert habe.
Vor ein paar Jahren schrieb ich in einem anderen Artikel :
Es ist wichtig zu bedenken, dass Best Practices für ein Werkzeug wie PHPUnit nicht in Stein gemeißelt sind. Vielmehr entwickeln sie sich im Laufe der Zeit weiter und müssen beispielsweise an Änderungen in PHP angepasst werden.
Damals ging es um das Testen von Ausnahmen, jetzt um das Verwalten von Testinventar. Letzteres werde ich fortan ohne die Methoden setUp()
und tearDown()
tun.