Die vollständige Reihenfolge von setUpBeforeClass bis tearDownAfterClass und was bei falscher Nutzung durchsickert
PHPUnit führt vier verschiedene Lifecycle-Hooks in einer festen Reihenfolge aus, zwei davon einmal pro Klasse, zwei davon vor und nach jedem einzelnen Test. Wer diese Reihenfolge nicht präzise kennt, baut leicht Tests, die scheinbar isoliert wirken, tatsächlich aber Zustand über static-Eigenschaften miteinander teilen. Dieser Artikel geht die komplette Kette durch, mit den typischen Fehlern und wann static- statt Instanz-Methoden nötig sind.
Inhaltsverzeichnis
- 1. Die vier Lifecycle-Phasen im Überblick
- 2. setUpBeforeClass: einmal pro Klasse, zwingend static
- 3. setUp und tearDown: der eigentliche Isolationsmechanismus
- 4. Der klassische Fehler: static-Zustand sickert zwischen Tests durch
- 5. Reihenfolge bei mehreren setUp-Implementierungen in Vererbungsketten
- 6. tearDown läuft in umgekehrter Reihenfolge relativ zu setUp
- 7. Wann static- statt Instanz-Methoden wirklich nötig sind
- 8. assertPreConditions und assertPostConditions als ergänzende Hooks
- 9. Fazit: Lifecycle-Disziplin als Grundlage für zuverlässige Tests
- 10. Zusammenfassung
- 11. FAQ
1. Die vier Lifecycle-Phasen im Überblick
PHPUnit durchläuft für jede Testklasse eine feste Abfolge von vier Phasen: setUpBeforeClass läuft genau einmal, bevor der erste Test der Klasse startet. Dann folgt für jeden einzelnen Test das Paar aus setUp, direkt vor dem Testkörper, und tearDown, direkt danach, egal ob der Test erfolgreich war oder fehlschlug. Zum Schluss läuft tearDownAfterClass genau einmal, nachdem der letzte Test der Klasse beendet wurde.
Diese Reihenfolge ist keine Implementierungsdetail-Trivia, sondern die Grundlage für korrekte Testisolation. Jeder Test soll unabhängig von der Ausführungsreihenfolge der anderen Tests dasselbe Ergebnis liefern, und genau dafür ist setUp/tearDown gedacht: Ein garantiert frischer Zustand vor jedem einzelnen Test. Wird dieses Prinzip verletzt, entstehen Tests, die nur bestehen, wenn sie in einer bestimmten Reihenfolge laufen, ein klassisches Zeichen für versteckte Kopplung.
2. setUpBeforeClass: einmal pro Klasse, zwingend static
setUpBeforeClass muss als static-Methode deklariert werden, weil sie läuft, bevor überhaupt eine Instanz der Testklasse existiert, PHPUnit instanziiert für jeden Test eine neue Objektinstanz, aber setUpBeforeClass gehört zeitlich davor. Typische Einsatzzwecke sind teure, einmalige Vorbereitungen: eine Testdatenbank-Verbindung aufbauen, eine Fixture-Datei einmalig parsen, einen Test-Container starten.
Der entscheidende Fallstrick ist, dass alles, was in setUpBeforeClass in einer static-Eigenschaft abgelegt wird, über alle Tests der Klasse hinweg geteilt wird. Verändert ein Test diesen Zustand, sieht der nächste Test bereits die veränderte Version, nicht den ursprünglichen. Deshalb gehört in setUpBeforeClass ausschließlich wirklich unveränderlicher oder read-only genutzter Zustand, niemals etwas, das ein Test während seiner Ausführung manipuliert.
<?php
declare(strict_types=1);
namespace Mironsoft\Tests\Integration;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use PDO;
final class OrderRepositoryDatabaseTest extends TestCase
{
private static PDO $connection;
public static function setUpBeforeClass(): void
{
// Einmalig teure Verbindung aufbauen, fuer alle Tests dieser Klasse
self::$connection = new PDO('sqlite::memory:');
self::$connection->exec('CREATE TABLE orders (id INTEGER PRIMARY KEY, sku TEXT)');
}
public static function tearDownAfterClass(): void
{
// Verbindung wieder freigeben, Referenz entfernen
unset(self::$connection);
}
#[Test]
public function speichertBestellungInDerDatenbank(): void
{
self::$connection->exec("INSERT INTO orders (sku) VALUES ('ABC-123')");
$count = self::$connection->query('SELECT COUNT(*) FROM orders')->fetchColumn();
self::assertSame(1, (int) $count);
}
}
3. setUp und tearDown: der eigentliche Isolationsmechanismus
setUp und tearDown laufen als Instanzmethoden vor und nach jedem einzelnen Test, weil PHPUnit für jeden Test eine frische Objektinstanz der Testklasse erzeugt. Instanzeigenschaften, die in setUp gesetzt werden, existieren also automatisch nur für die Dauer genau eines Tests, das ist der eingebaute Isolationsmechanismus, den viele Entwickler unterschätzen, weil er so selbstverständlich funktioniert.
tearDown läuft garantiert auch dann, wenn der Test mit einer Exception fehlschlägt oder eine Assertion nicht erfüllt wurde, PHPUnit fängt intern ab und ruft tearDown trotzdem auf. Das macht tearDown zum richtigen Ort für Aufräumarbeiten, die auch bei einem gescheiterten Test passieren müssen, etwa das Schließen einer temporären Datei oder das Zurücksetzen eines globalen Zustands, der außerhalb von PHPUnits eigener Kontrolle liegt.
<?php
declare(strict_types=1);
namespace Mironsoft\Tests\Unit;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class ShoppingCartTest extends TestCase
{
private ShoppingCart $cart;
protected function setUp(): void
{
// Neue Instanz vor JEDEM Test, garantiert frischer Zustand
$this->cart = new ShoppingCart();
}
protected function tearDown(): void
{
// Laeuft auch bei fehlgeschlagenem Test
$this->cart->clear();
}
#[Test]
public function fuegtProduktHinzu(): void
{
$this->cart->add('SKU-1', 2);
self::assertSame(2, $this->cart->quantityOf('SKU-1'));
}
#[Test]
public function warenkorbIstBeiJedemTestLeer(): void
{
// Kein Ueberbleibsel aus dem vorherigen Test, dank setUp
self::assertTrue($this->cart->isEmpty());
}
}
4. Der klassische Fehler: static-Zustand sickert zwischen Tests durch
Der häufigste Lifecycle-Bug entsteht, wenn ein Entwickler eine static-Eigenschaft nutzt, um sich das wiederholte Aufsetzen in setUp zu ersparen, und dabei übersieht, dass diese Eigenschaft dann von allen Tests der Klasse geteilt wird. Läuft Test A zuerst und verändert den static-Zustand, sieht Test B bereits die veränderte Version, obwohl B davon ausgeht, mit einem unberührten Ausgangszustand zu starten.
Solche Bugs sind besonders tückisch, weil sie oft erst auffallen, wenn sich die Ausführungsreihenfolge ändert, etwa durch ein PHPUnit-Update, eine neue Testdatei im selben Verzeichnis oder die Aktivierung von zufälliger Testreihenfolge über --order-by=random. Ein Test, der isoliert grün ist, aber in der vollen Suite rot wird, ist fast immer ein Hinweis auf genau dieses Muster.
<?php
declare(strict_types=1);
namespace Mironsoft\Tests\Unit;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
// FALSCH: static-Zustand sickert zwischen Tests durch
final class CounterLeakyTest extends TestCase
{
private static int $counter = 0;
#[Test]
public function ersterAufruf(): void
{
self::$counter++;
self::assertSame(1, self::$counter); // besteht nur, wenn dieser Test zuerst laeuft
}
#[Test]
public function zweiterAufruf(): void
{
self::$counter++;
self::assertSame(1, self::$counter); // schlaegt fehl, wenn Reihenfolge sich aendert
}
}
// RICHTIG: Zustand gehoert in eine Instanzeigenschaft, per setUp zurueckgesetzt
final class CounterIsolatedTest extends TestCase
{
private int $counter = 0;
protected function setUp(): void
{
$this->counter = 0;
}
#[Test]
public function ersterAufruf(): void
{
$this->counter++;
self::assertSame(1, $this->counter);
}
}
5. Reihenfolge bei mehreren setUp-Implementierungen in Vererbungsketten
Erbt eine Testklasse von einer abstrakten Basisklasse, die selbst eine setUp-Methode definiert, muss die überschreibende setUp-Methode in der Kindklasse zwingend parent::setUp() aufrufen, PHPUnit ruft nicht automatisch beide Implementierungen auf. Vergisst man diesen Aufruf, geht die Vorbereitung der Basisklasse verloren, ohne dass PHPUnit eine Warnung ausgibt, ein besonders leicht zu übersehender Fehler bei mehrstufigen Testklassen-Hierarchien.
Die Ausführungsreihenfolge bei explizitem parent::setUp()-Aufruf hängt davon ab, ob dieser Aufruf am Anfang oder am Ende der Kindklassen-Methode steht. Bewährt hat sich, parent::setUp() konsequent als erste Zeile zu platzieren, sodass die Basisvorbereitung immer vor der spezifischen Vorbereitung der Kindklasse läuft, in Analogie zur üblichen Konstruktor-Konvention mit parent::__construct().
<?php
declare(strict_types=1);
namespace Mironsoft\Tests\Unit;
abstract class DatabaseTestCase extends \PHPUnit\Framework\TestCase
{
protected \PDO $connection;
protected function setUp(): void
{
$this->connection = new \PDO('sqlite::memory:');
}
}
final class OrderRepositoryTest extends DatabaseTestCase
{
private OrderRepository $repository;
protected function setUp(): void
{
parent::setUp(); // Basisvorbereitung MUSS zuerst laufen
$this->repository = new OrderRepository($this->connection);
}
}
6. tearDown läuft in umgekehrter Reihenfolge relativ zu setUp
Ein weniger bekanntes Detail betrifft tearDown in Vererbungsketten: Auch hier muss die Kindklasse parent::tearDown() explizit aufrufen, aber die empfohlene Konvention ist umgekehrt zu setUp, parent::tearDown() sollte am Ende der überschreibenden Methode stehen. So wird Zustand in derselben Reihenfolge aufgeräumt, in der er aufgebaut wurde, zuletzt aufgebaut heißt zuerst wieder abgebaut, ein Muster, das aus Stack-basierten Ressourcen-Verwaltungen bekannt ist.
In der Praxis ist dieses Detail vor allem relevant, wenn tearDown in mehreren Ebenen Ressourcen wie Dateihandles oder Netzwerkverbindungen schließt, die voneinander abhängen. Wird die Basisklassen-Verbindung zu früh geschlossen, während die Kindklasse noch etwas über sie aufräumen möchte, entsteht eine Exception im tearDown selbst, was PHPUnit als separaten Fehler zusätzlich zum eigentlichen Testergebnis meldet.
7. Wann static- statt Instanz-Methoden wirklich nötig sind
static gehört ausschließlich zu setUpBeforeClass und tearDownAfterClass, weil diese vor der ersten beziehungsweise nach der letzten Instanziierung laufen. Der berechtigte Einsatzzweck für static-Zustand ist eine teure, aber unveränderliche Ressource, ein hochgefahrener Test-Container, eine einmal geparste, große Fixture-Datei, eine In-Memory-Datenbankverbindung, deren Schema sich während der gesamten Testklasse nicht ändert.
Sobald ein Test diesen static-Zustand aktiv verändert, etwa Zeilen in eine Tabelle schreibt, gehört eine explizite Rücksetzung in tearDown, die den ursprünglichen Zustand wiederherstellt, zum Beispiel durch eine Transaktion, die pro Test gestartet und in tearDown zurückgerollt wird. Diese Kombination aus static-Ressource in setUpBeforeClass und Transaktions-Rollback in tearDown ist ein bewährtes Muster für performante, aber trotzdem isolierte Datenbanktests.
<?php
declare(strict_types=1);
namespace Mironsoft\Tests\Integration;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use PDO;
final class OrderRepositoryTransactionalTest extends TestCase
{
private static PDO $connection;
public static function setUpBeforeClass(): void
{
self::$connection = new PDO('sqlite::memory:');
self::$connection->exec('CREATE TABLE orders (id INTEGER PRIMARY KEY, sku TEXT)');
}
protected function setUp(): void
{
self::$connection->beginTransaction();
}
protected function tearDown(): void
{
// Rollback stellt den Ausgangszustand fuer den naechsten Test wieder her
self::$connection->rollBack();
}
#[Test]
public function speichertBestellung(): void
{
self::$connection->exec("INSERT INTO orders (sku) VALUES ('X-1')");
self::assertSame(1, (int) self::$connection->query('SELECT COUNT(*) FROM orders')->fetchColumn());
}
}
8. assertPreConditions und assertPostConditions als ergänzende Hooks
Neben den vier Haupt-Hooks bietet PHPUnit zwei weniger bekannte Methoden, assertPreConditions, die direkt nach setUp läuft, und assertPostConditions, die direkt vor tearDown läuft. Beide sind speziell dafür gedacht, Bedingungen zu prüfen, die logisch zum Testaufbau gehören, aber getrennt vom eigentlichen Testkörper stehen sollen, etwa eine generelle Prüfung, dass die Datenbankverbindung nach dem Aufbau tatsächlich aktiv ist.
In der Praxis werden diese beiden Hooks selten benötigt, weil ihre Aufgabe meist ebenso gut in setUp beziehungsweise am Ende der Testmethode erledigt werden kann. Sinnvoll sind sie vor allem in Basisklassen, die von vielen Testklassen geerbt werden, und dort eine Invariante prüfen wollen, ohne dass jede einzelne Kindklasse diese Prüfung explizit wiederholen muss, etwa dass nach jedem Test keine offene Datenbanktransaktion zurückbleibt.
9. Fazit: Lifecycle-Disziplin als Grundlage für zuverlässige Tests
Die Lifecycle-Reihenfolge von PHPUnit ist an sich einfach, setUpBeforeClass, dann pro Test setUp und tearDown, am Ende tearDownAfterClass. Die Fehleranfälligkeit entsteht ausschließlich durch static-Zustand, der eigentlich instanzbezogen sein sollte, oder durch vergessene parent-Aufrufe in Vererbungsketten. Beides lässt sich durch eine klare Regel vermeiden: static ausschließlich für wirklich unveränderlichen oder transaktional zurückgesetzten Zustand.
Wer Tests schreibt, die zufällig in einer bestimmten Reihenfolge laufen müssen, um grün zu bleiben, hat meist genau dieses Prinzip verletzt. Ein guter Praxistest ist, die Suite regelmäßig mit --order-by=random laufen zu lassen, dabei fallen durchsickernde Abhängigkeiten fast immer sofort auf, lange bevor sie in Produktion zu einem echten Problem werden.
| Hook | Häufigkeit | static erforderlich | Typischer Einsatz |
|---|---|---|---|
| setUpBeforeClass | Einmal pro Klasse, vor allen Tests | Ja | Teure, unveränderliche Ressource aufbauen |
| setUp | Vor jedem einzelnen Test | Nein | Frischen Instanzzustand herstellen |
| tearDown | Nach jedem einzelnen Test | Nein | Aufräumen, auch bei Fehlschlag |
| tearDownAfterClass | Einmal pro Klasse, nach allen Tests | Ja | Ressource endgültig freigeben |
Mironsoft
Testautomatisierung, Magento-Qualitätssicherung und CI-Integration
Tests, die echte Fehler finden statt nur grün zu leuchten?
Wir prüfen bestehende PHPUnit-Suiten auf Implementierungsdetail-Tests, flaky Tests und fehlende Coverage an kritischen Stellen und bauen daraus eine Teststrategie, die bei jedem Magento-Update wirklich Sicherheit gibt.
Test-Audit
Bestehende Suiten auf Mocking-Antipatterns und blinde Flecken prüfen.
Teststrategie
Unit-, Integrations- und MFTF-Tests sinnvoll für Magento-Projekte kombinieren.
CI-Integration
Schnelle, zuverlässige Testläufe in GitLab CI oder GitHub Actions einrichten.
10. Zusammenfassung
PHPUnit Lifecycle Hooks: Das Wichtigste auf einen Blick
Kernidee
setUpBeforeClass läuft einmal pro Klasse, setUp und tearDown vor und nach jedem einzelnen Test.
Größter Fallstrick
static-Eigenschaften teilen Zustand über alle Tests, wenn sie während der Ausführung verändert werden.
Vererbung
parent::setUp() gehört an den Anfang, parent::tearDown() ans Ende der überschreibenden Methode.
Diagnose-Tipp
--order-by=random deckt durchsickernden Zustand fast immer sofort auf.