Fehlgeschlagene Tests automatisch wiederholen: Flaky-Test-Retry-Plugins in PHPUnit
AI generated
@test
assert
PHPUnit · Flaky Tests · CI/CD
Fehlgeschlagene Tests automatisch wiederholen
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.

14 Min. Lesezeit Flaky Tests · Retry · CI/CD PHPUnit 10 · 11 · PHP 8.x

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.

11. FAQ: Retry-Plugins fuer flaky Tests: Das Wichtigste auf einen Blick

1Ist es grundsaetzlich falsch, Retry-Mechanismen in PHPUnit einzusetzen?
Nein, in klar begrenzten Faellen wie instabilen externen Diensten ist Retry eine legitime Uebergangsmassnahme. Problematisch wird es erst, wenn Retry dauerhaft statt der eigentlichen Ursachenbehebung eingesetzt wird.
2Wie unterscheidet sich Retry auf Testebene von einem erneuten Pipeline-Lauf?
Retry auf Testebene wiederholt nur die betroffene Testmethode innerhalb desselben Laufs, ein erneuter Pipeline-Lauf startet die gesamte Suite neu. Ersteres ist deutlich schneller und praeziser.
3Sollte ich Retry fuer Unit-Tests gegen meinen eigenen Code verwenden?
Nein. Unit-Tests gegen deterministischen, eigenen Code sollten niemals flaky sein. Wenn sie es doch sind, liegt ein echtes Problem im Testdesign oder im getesteten Code vor, das behoben werden muss, nicht uebertoent.
4Wie viele Retry-Versuche sind sinnvoll?
Meist reichen zwei bis drei Versuche. Mehr Versuche verschleiern das Problem zunehmend und kosten unnoetig Laufzeit, ohne die eigentliche Ursache naeher zu bringen.
5Kann Retry echte Bugs vor dem Team verstecken?
Ja, das ist das groesste Risiko. Ein echter, reproduzierbarer Fehler kann faelschlich als Flakiness eingestuft und durch Retry ueberdeckt werden, wenn niemand die Wiederholungen aktiv auswertet.
6Wie protokolliere ich, wie oft ein Test einen Retry braucht?
Ueber die Auswertung der JUnit-XML-Reports in der CI-Pipeline oder durch eigenes Logging innerhalb der Retry-Logik, das die Anzahl der Versuche pro Test erfasst und langfristig auswertbar macht.
7Ist eine eigene, manuelle Retry-Loesung besser als ein fertiges Plugin?
Beides hat Vor- und Nachteile. Eine manuelle Loesung ist sichtbarer im Code und verhindert versehentlichen Overuse, ein fertiges Plugin spart Schreibarbeit, kann aber leichter unbedacht auf zu viele Tests angewendet werden.
8Was tue ich, wenn ein Test trotz mehrfachem Retry weiterhin gelegentlich fehlschlaegt?
Das ist ein starkes Signal, dass die Ursache tiefer liegt als angenommen und priorisiert untersucht werden sollte, statt die Anzahl der Retry-Versuche einfach zu erhoehen.
9Sollte jede Retry-Annotation ein Ablaufdatum oder Ticket haben?
Ja, das ist eine wichtige Disziplinmassnahme. Ohne diese Verpflichtung wird aus einer befristeten Uebergangsloesung schnell ein dauerhafter, unhinterfragter Zustand.
10Loesen Retry-Plugins Race Conditions bei parallel laufenden Tests?
Nein, sie verschleiern sie hoechstens. Race Conditions sollten durch isolierte Fixtures oder gezielte Deaktivierung der Parallelisierung fuer die betroffenen Tests behoben werden, nicht durch Wiederholung.