Was Retry-Plugins in PHPUnit leisten und wo ihre Grenze liegt
Manche Tests scheitern nicht, weil der getestete Code fehlerhaft ist, sondern weil sie von Faktoren abhaengen, die sich der Kontrolle des Tests entziehen: Netzwerklatenz, Timing-Fenster oder externe Dienste mit gelegentlichen Aussetzern. Retry-Plugins wiederholen einen fehlgeschlagenen Test automatisch, bevor sie ihn endgueltig als rot werten, und koennen so eine instabile CI-Pipeline kurzfristig stabilisieren. Wer Retry aber als dauerhafte Loesung statt als befristete Uebergangsmassnahme einsetzt, verschiebt echte Probleme nur in eine bequeme Grauzone.
Inhaltsverzeichnis
- 1. Was macht einen Test flaky
- 2. Wie Retry-Mechanismen technisch funktionieren
- 3. Eine schlanke eigene Retry-Loesung ohne Zusatzpaket
- 4. Retry ist Symptombehandlung, kein Ersatz fuer die Ursachenanalyse
- 5. Wann Retry als befristete Uebergangsloesung vertretbar ist
- 6. Typische Ursachen und ihre eigentliche Behebung
- 7. Haeufige Konfigurationsfallen beim Einsatz von Retry
- 8. Flakiness sichtbar machen statt sie im Retry verschwinden zu lassen
- 9. Team-Regeln fuer den verantwortungsvollen Einsatz von Retry
- 10. Zusammenfassung
- 11. FAQ
1. Was macht einen Test flaky
Ein flaky Test ist ein Test, der bei unveraendertem Code und unveraendertem Testcode mal gruen und mal rot wird. Das unterscheidet ihn fundamental von einem echten Fehlschlag, bei dem eine reale Regression im Produktivcode die Ursache ist. Typische Ausloeser fuer Flakiness sind Zeitabhaengigkeiten, etwa ein Test, der auf eine feste Ausfuehrungszeit unter einer Sekunde angewiesen ist, aber unter Last laenger dauert, Netzwerkzugriffe auf externe Dienste mit gelegentlichen Timeouts, oder Race Conditions bei parallel laufenden Tests, die auf dieselbe Ressource zugreifen.
Das Kernproblem flaky Tests ist nicht nur die einzelne rote Pipeline, sondern der schleichende Vertrauensverlust im Team. Sobald Entwickler gelernt haben, dass ein fehlgeschlagener Build oft einfach nur einen erneuten Lauf braucht, beginnen sie, echte Fehlschlaege reflexartig zu ignorieren und die Pipeline einfach neu zu starten. Genau in diesem Moment verliert die Testsuite ihre eigentliche Funktion als verlaessliches Signal, und ein echter Bug kann monatelang unbemerkt durchrutschen, weil niemand mehr genau hinschaut.
2. Wie Retry-Mechanismen technisch funktionieren
Ein Retry-Plugin fuer PHPUnit, etwa ueber ein Attribut oder eine Annotation an der Testmethode, sorgt dafuer, dass ein fehlgeschlagener Test nicht sofort als endgueltig gescheitert gilt, sondern innerhalb desselben Laufs bis zu einer konfigurierten Anzahl weiterer Male ausgefuehrt wird. Erst wenn auch der letzte Versuch fehlschlaegt, meldet PHPUnit den Test als tatsaechlich rot. Besteht ein Wiederholungsversuch, gilt der Test insgesamt als erfolgreich, meist mit einem Hinweis in der Ausgabe, dass eine Wiederholung stattgefunden hat.
Wichtig ist der Unterschied zu einem einfachen erneuten Start der gesamten Pipeline: Retry-Plugins arbeiten auf Testebene, nicht auf Pipeline-Ebene. Das spart erheblich Zeit, weil nicht die komplette Suite neu laufen muss, nur um einen einzelnen instabilen Test zu wiederholen, sondern nur die betroffene Methode selbst erneut ausgefuehrt wird, oft innerhalb weniger Millisekunden bis Sekunden zusaetzlicher Laufzeit.
use PHPUnit\Framework\Attributes\Test;
final class ExternalWeatherApiTest extends \PHPUnit\Framework\TestCase
{
// Beispiel mit einem Retry-faehigen Test-Runner (z. B. dodevo/retry-annotation
// oder ein selbstgeschriebener PHPUnit-Extension-Hook).
/**
* @retry 3
* @retryDelayMethod waitMilliseconds
*/
public function testCurrentTemperatureIsFetchedSuccessfully(): void
{
$client = new ExternalWeatherApiClient();
$temperature = $client->fetchCurrentTemperature('Hamburg');
self::assertIsFloat($temperature);
}
protected function waitMilliseconds(int $attempt): void
{
usleep($attempt * 200_000);
}
}
3. Eine schlanke eigene Retry-Loesung ohne Zusatzpaket
Nicht jedes Projekt moechte eine zusaetzliche Abhaengigkeit fuer Retry-Logik einfuehren. Eine einfache Alternative laesst sich mit einer kleinen Helper-Methode umsetzen, die einen Codeblock so oft wiederholt, bis er entweder erfolgreich ist oder die maximale Anzahl an Versuchen erreicht wurde. Diese Loesung ist bewusst manuell und explizit sichtbar im Testcode, was einen wichtigen Vorteil hat: Niemand kann versehentlich uebersehen, dass ein Test wiederholt wird, weil es direkt im Testkoerper steht statt in einer unsichtbaren Annotation.
Der Nachteil dieser manuellen Loesung ist, dass sie mehr Schreibarbeit erfordert und pro Test angepasst werden muss. Fuer Projekte mit nur wenigen bekannt instabilen Tests ist das ein akzeptabler Kompromiss, weil die Sichtbarkeit im Code hoeher ist als bei einer global wirkenden Retry-Annotation, die leicht auf zu viele Tests angewendet wird, ohne dass es jemandem auffaellt.
trait RetriesFlakyAssertions
{
/**
* Fuehrt einen Codeblock bis zu $maxAttempts Mal aus, bis er ohne
* Exception durchlaeuft. Nur fuer bekannt instabile, externe Abhaengigkeiten
* einsetzen, niemals als Standardvorgehen fuer alle Tests.
*/
protected function retryFlaky(callable $callback, int $maxAttempts = 3, int $delayMs = 200): void
{
$lastException = null;
for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
try {
$callback();
return;
} catch (\Throwable $exception) {
$lastException = $exception;
usleep($delayMs * 1000);
}
}
throw $lastException;
}
}
final class ExternalWeatherApiTest extends \PHPUnit\Framework\TestCase
{
use RetriesFlakyAssertions;
public function testCurrentTemperatureIsFetchedSuccessfully(): void
{
$client = new ExternalWeatherApiClient();
$this->retryFlaky(function () use ($client): void {
$temperature = $client->fetchCurrentTemperature('Hamburg');
self::assertIsFloat($temperature);
});
}
}
4. Retry ist Symptombehandlung, kein Ersatz fuer die Ursachenanalyse
Der wichtigste Grundsatz beim Einsatz von Retry-Mechanismen lautet: Ein Retry macht einen instabilen Test bequemer, nicht stabiler. Die eigentliche Ursache, etwa ein externer Dienst ohne Timeout-Handling, eine Race Condition bei parallelen Tests, oder ein zu knapp bemessenes Zeitfenster fuer eine asynchrone Operation, bleibt unveraendert bestehen. Retry versteckt das Problem lediglich vor dem CI-Dashboard, loest es aber nicht.
Besonders gefaehrlich wird Retry, wenn es unreflektiert auf immer mehr Tests angewendet wird, weil es kurzfristig die einfachste Loesung fuer eine rote Pipeline ist. Ein Team, das diesen Weg konsequent geht, baut sich ueber Monate eine Testsuite, die formal gruen ist, aber in Wahrheit viele latente Probleme verbirgt. Kommt es dann zu einem echten Produktionsfehler, der urspruenglich durch einen flaky Test haette erkannt werden koennen, war die Retry-Logik indirekt mitverantwortlich, weil sie das Warnsignal wiederholt uebertoent hat.
5. Wann Retry als befristete Uebergangsloesung vertretbar ist
Retry ist nicht grundsaetzlich falsch, sondern in bestimmten, klar begrenzten Situationen eine legitime Uebergangsmassnahme. Ein Beispiel ist ein Integrationstest gegen einen externen Drittanbieter-Dienst, dessen gelegentliche kurze Ausfaelle ausserhalb der eigenen Kontrolle liegen, waehrend gleichzeitig ein Ticket fuer eine robustere Loesung, etwa ein Mock oder ein Contract-Test, bereits existiert. In diesem Fall ueberbrueckt Retry die Zeit bis zur eigentlichen Behebung, ohne die Pipeline unnoetig zu blockieren.
Entscheidend ist, dass jeder Retry-Einsatz mit einer klaren Begruendung und einem Ablaufdatum versehen wird, etwa in Form eines verlinkten Tickets im Code-Kommentar. Ohne diese Disziplin wird aus einer befristeten Uebergangsloesung schnell ein dauerhafter Zustand, weil niemand mehr aktiv daran arbeitet, das Retry wieder zu entfernen, sobald die eigentliche Ursache behoben ist.
final class ThirdPartyShippingRateApiTest extends \PHPUnit\Framework\TestCase
{
/**
* Retry als befristete Uebergangsloesung.
* Ursache: Drittanbieter-API hat sporadische 2-Sekunden-Timeouts.
* Tracking: JIRA-4821, geplante Loesung: Contract-Test mit gemocktem Client.
*
* @retry 2
*/
public function testShippingRateIsCalculatedForGermany(): void
{
$client = new ThirdPartyShippingRateApiClient();
$rate = $client->calculateRate('DE', 2.5);
self::assertGreaterThan(0.0, $rate);
}
}
6. Typische Ursachen und ihre eigentliche Behebung
Um Retry gezielt und begrenzt einzusetzen, hilft es, die haeufigsten Ursachen fuer Flakiness zu kennen und ihre jeweils passende, nachhaltige Behebung parallel zu verfolgen. Zeitabhaengige Tests lassen sich meist durch explizites Warten auf einen definierten Zustand statt auf eine feste Anzahl Millisekunden stabilisieren, etwa mit einem Polling-Mechanismus, der wiederholt prueft, ob eine Bedingung erfuellt ist, statt blind eine feste Wartezeit anzunehmen.
Race Conditions bei parallel laufenden Tests entstehen oft durch gemeinsam genutzte Ressourcen, etwa dieselbe Testdatenbank oder dieselbe Datei, und lassen sich durch isolierte Fixtures pro Test oder durch das Deaktivieren der Parallelisierung fuer die betroffene Testklasse beheben. Netzwerkabhaengige Tests gegen instabile Drittanbieter sollten mittelfristig durch Contract-Tests mit gemockten Antworten ersetzt werden, die die Vertragsbedingungen der Schnittstelle pruefen, ohne von deren tatsaechlicher Verfuegbarkeit abzuhaengen.
7. Haeufige Konfigurationsfallen beim Einsatz von Retry
Ein verbreiteter Fehler ist, Retry nicht gezielt auf einzelne, bekannt instabile Tests anzuwenden, sondern global fuer die gesamte Suite zu aktivieren, etwa ueber eine pauschale Konfiguration im Test-Runner. Das mag kurzfristig die Anzahl roter Pipelines senken, verschleiert aber systematisch, welche Tests tatsaechlich ein Problem haben, weil selbst ein Test, der aus einem echten Bug heraus fehlschlaegt, durch einen zufaelligen zweiten Versuch gruen werden kann, wenn der Bug selbst nicht deterministisch ausloest.
Ein zweiter haeufiger Fehler ist eine zu kurze oder fehlende Wartezeit zwischen den Wiederholungsversuchen. Wird ein Test sofort ohne Verzoegerung erneut ausgefuehrt, bleibt die urspruengliche Ursache, etwa ein kurzzeitig ueberlasteter externer Dienst, oft weiterhin bestehen, und auch der zweite Versuch scheitert. Eine sinnvolle, leicht ansteigende Wartezeit zwischen den Versuchen erhoeht die Chance, dass sich eine voruebergehende Stoerung tatsaechlich aufgeloest hat, bevor der naechste Versuch startet.
8. Flakiness sichtbar machen statt sie im Retry verschwinden zu lassen
Damit Retry nicht zur dauerhaften Verschleierung wird, sollte jede Wiederholung protokolliert und ausgewertet werden. Viele CI-Systeme erlauben es, die Anzahl der Retry-Ereignisse pro Test ueber die Zeit zu sammeln, etwa durch Auswertung der JUnit-XML-Reports oder durch ein eigenes kleines Logging innerhalb der Retry-Logik. Ein Test, der wiederholt einen Retry benoetigt, ist ein klares Signal, dass er priorisiert behoben werden sollte, statt weiterhin toleriert zu werden.
Ein sinnvolles Dashboard zeigt fuer jeden Test, wie oft er in den letzten Wochen ein Retry gebraucht hat, und macht diese Zahl fuer das gesamte Team sichtbar, etwa in einem woechentlichen Review. So bleibt Flakiness ein aktiv verfolgtes technisches Anliegen statt eines stillen Hintergrundrauschens, das irgendwann als normaler Zustand akzeptiert wird.
9. Team-Regeln fuer den verantwortungsvollen Einsatz von Retry
Ein Team sollte klar festlegen, unter welchen Bedingungen Retry ueberhaupt genutzt werden darf: typischerweise nur fuer Tests gegen externe Systeme ausserhalb der eigenen Kontrolle, niemals fuer Unit-Tests gegen den eigenen, deterministischen Code. Jede Retry-Annotation sollte verpflichtend einen Kommentar mit Begruendung und Ticket-Referenz tragen, damit die Massnahme nachvollziehbar bleibt und nicht in Vergessenheit geraet.
Zusaetzlich lohnt sich eine regelmaessige Bereinigung: Alle paar Monate sollte ein Team gezielt alle aktiven Retry-Annotationen durchgehen und pruefen, ob die urspruengliche Ursache inzwischen behoben wurde und das Retry entfernt werden kann. Die folgende Tabelle fasst gaengige Ursachen fuer Flakiness, ihre nachhaltige Behebung und die Rolle zusammen, die Retry dabei jeweils spielen sollte.
| Ursache | Symptom | Nachhaltige Behebung | Rolle von Retry |
|---|---|---|---|
| Feste Wartezeit statt Zustandspruefung | Test schlaegt unter Last gelegentlich fehl | Polling auf definierten Zustand statt fixer Sleep-Dauer | Nur kurzfristig, waehrend Fix umgesetzt wird |
| Race Condition bei paralleler Ausfuehrung | Zufaellige Fehlschlaege je nach Ausfuehrungsreihenfolge | Isolierte Fixtures pro Test, ggf. Parallelisierung deaktivieren | Nicht empfohlen, verschleiert die Race Condition |
| Externer Drittanbieter-Dienst instabil | Sporadische Timeouts oder 5xx-Fehler | Contract-Test mit gemocktem Client | Vertretbar, befristet mit Ticket-Referenz |
| Uneindeutige Testreihenfolge | Test schlaegt nur in bestimmter Reihenfolge fehl | Tests unabhaengig voneinander machen, keine geteilten Zustaende | Nicht empfohlen, Ursache liegt im Testdesign |
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
Retry-Plugins fuer flaky Tests: Das Wichtigste auf einen Blick
Definition
Ein flaky Test liefert bei unveraendertem Code mal gruene, mal rote Ergebnisse, meist wegen Zeit-, Netzwerk- oder Nebenlaeufigkeitsproblemen.
Retry-Mechanik
Retry-Plugins wiederholen einen fehlgeschlagenen Test auf Methodenebene innerhalb desselben Laufs, bevor er als endgueltig rot gilt.
Grundregel
Retry macht einen Test bequemer, nicht stabiler, die eigentliche Ursache bleibt bestehen und muss separat behoben werden.
Vertretbarer Einsatz
Nur befristet, mit Begruendung und Ticket-Referenz, typischerweise fuer externe Abhaengigkeiten ausserhalb der eigenen Kontrolle.