Wie können fehlgeschlagene JUnit-Tests sofort erneut ausgeführt werden?

81

Gibt es eine Möglichkeit, eine JUnit-Regel oder ähnliches zu haben, die jedem fehlgeschlagenen Test eine zweite Chance gibt, indem Sie einfach versuchen, ihn erneut auszuführen?

Hintergrund: Ich habe eine große Anzahl von Selenium2-WebDriver-Tests mit JUnit geschrieben. Aufgrund eines sehr aggressiven Timings (nur kurze Wartezeiten nach den Klicks) können einige Tests (1 von 100 und immer ein anderer) fehlschlagen, da der Server manchmal etwas langsamer reagiert. Aber ich kann die Wartezeit nicht so lang machen, dass sie definitiv lang genug ist, denn dann werden die Tests ewig dauern.) - Ich denke, es ist für diesen Anwendungsfall akzeptabel, dass ein Test grün ist, selbst wenn er eine Sekunde benötigt Versuchen.

Natürlich wäre es besser, eine 2 von 3 Mehrheit zu haben (wiederholen Sie einen fehlgeschlagenen Test dreimal und nehmen Sie sie als richtig, wenn zwei der Tests korrekt sind), aber dies wäre eine zukünftige Verbesserung.

Ralph
quelle
1
Es sollte nicht erforderlich sein, feste Wartezeiten in Selen 2 festzulegen. Der WebDriver sollte das Laden von Seiten erkennen und entsprechend warten. Wenn Sie auf etwas anderes als das Laden von Seiten warten möchten, beispielsweise auf die Ausführung von JavaScript, sollten Sie die WebDriverWait-Klasse verwenden, siehe: seleniumhq.org/docs/04_webdriver_advanced.html . Trotzdem denke ich, dass es in Ordnung sein kann, GUI-Tests erneut zu versuchen. Ich wollte nur klarstellen, dass in den meisten Fällen keine explizite Wartezeit erforderlich ist.
Tim Büthe
Es ist wahr, aber ich werde auch darauf hinweisen, dass ich an einigen wirklich, wirklich schlechten Servern gearbeitet habe, die "in Ordnung" sind, aber auf bestimmten Seiteninstanzen eine WIRKLICH lange Hochlaufzeit haben , und deshalb möchte ich nicht Versagen. Das ist eine großartige Frage, danke. (Natürlich würde ich es vorziehen, wenn das Timing IMMER konsistent ist, und wir werden darauf drängen, aber bis dahin muss dies reichen)
cgp
Wenn Sie die Funktion cucumber rerun.txt verwenden, finden Sie meine Antwort hier
Sugat Mankar
Wenn Sie die Funktion Cucumber rerun.txt verwenden, lesen Sie bitte ans hier.
Sugat Mankar

Antworten:

105

Sie können dies mit einer TestRule tun . Dies gibt Ihnen die Flexibilität, die Sie benötigen. Mit einer TestRule können Sie Logik um den Test einfügen, sodass Sie die Wiederholungsschleife implementieren würden:

public class RetryTest {
    public class Retry implements TestRule {
        private int retryCount;

        public Retry(int retryCount) {
            this.retryCount = retryCount;
        }

        public Statement apply(Statement base, Description description) {
            return statement(base, description);
        }

        private Statement statement(final Statement base, final Description description) {
            return new Statement() {
                @Override
                public void evaluate() throws Throwable {
                    Throwable caughtThrowable = null;

                    // implement retry logic here
                    for (int i = 0; i < retryCount; i++) {
                        try {
                            base.evaluate();
                            return;
                        } catch (Throwable t) {
                            caughtThrowable = t;
                            System.err.println(description.getDisplayName() + ": run " + (i+1) + " failed");
                        }
                    }
                    System.err.println(description.getDisplayName() + ": giving up after " + retryCount + " failures");
                    throw caughtThrowable;
                }
            };
        }
    }

    @Rule
    public Retry retry = new Retry(3);

    @Test
    public void test1() {
    }

    @Test
    public void test2() {
        Object o = null;
        o.equals("foo");
    }
}

Das Herzstück von a TestRuleist das base.evaluate(), das Ihre Testmethode aufruft. Um diesen Aufruf herum setzen Sie eine Wiederholungsschleife. Wenn in Ihrer Testmethode eine Ausnahme ausgelöst wird (ein Assertionsfehler ist tatsächlich ein Fehler AssertionError), ist der Test fehlgeschlagen, und Sie werden es erneut versuchen.

Es gibt noch eine andere Sache, die von Nutzen sein kann. Möglicherweise möchten Sie diese Wiederholungslogik nur auf eine Reihe von Tests anwenden. In diesem Fall können Sie der Wiederholungsklasse über einem Test eine bestimmte Anmerkung zur Methode hinzufügen. Descriptionenthält eine Liste von Anmerkungen für die Methode. Weitere Informationen hierzu finden Sie in meiner Antwort auf So führen Sie Code vor jeder JUnit @ Test-Methode einzeln aus, ohne @RunWith oder AOP zu verwenden. .

Verwenden eines benutzerdefinierten TestRunner

Dies ist der Vorschlag von CKuck, Sie können Ihren eigenen Runner definieren. Sie müssen BlockJUnit4ClassRunner erweitern und runChild () überschreiben. Weitere Informationen finden Sie in meiner Antwort auf Definieren der JUnit-Methodenregel in einer Suite. . Diese Antwort beschreibt, wie Sie definieren, wie Code für jede Methode in einer Suite ausgeführt wird, für die Sie Ihren eigenen Runner definieren müssen.

Matthew Farwell
quelle
Danke: Übrigens für jeden, der dies versuchen wird. TestRule ist eine Funktion, die seit JUnit Version 4.9
Ralph
@Ralph Tatsächlich ist TestRule ein Ersatz für MethodRule, das zuvor eingeführt wurde, etwa 4.7 IIRC, sodass diese Lösung möglicherweise vor 4.9 angewendet werden kann, sich jedoch geringfügig unterscheidet.
Matthew Farwell
7
Das war wirklich hilfreich, aber etwas, das mir in den Sinn kam: retryCount und Wiederholungsversuche könnten irreführende Namen sein. Wenn Wiederholung 1 ist, würde ich annehmen, dass er den Test ausführt, und wenn er fehlschlägt, wiederholt er ihn einmal, aber das ist nicht der Fall. Die Variable sollte wahrscheinlich maxTries heißen.
Thomas M.
@MatthewFarwell: Startet dies die Aktivität neu? Gibt es eine Möglichkeit, das zu tun?
Basim Sherif
3
Die Verwendung dieser Methode unterliegt der Einschränkung, dass die Testwiederholungen durchgeführt werden, ohne dass die Testinstanz neu erstellt wird. Das bedeutet, dass Instanzfelder in der Testklasse (oder in den Superklassen) nicht neu initialisiert werden und möglicherweise den Status der früheren Durchläufe verlassen.
Jonah Graham
19

Jetzt gibt es eine bessere Option. Wenn Sie Maven-Plugins wie Surfire oder Failsefe verwenden, können Sie den Parameter rerunFailingTestsCount SurFire Api hinzufügen . Dieses Zeug wurde im folgenden Ticket implementiert: Jira Ticket . In diesem Fall müssen Sie Ihren benutzerdefinierten Code nicht schreiben und das Plugin ändert den Testergebnisbericht automatisch.
Ich sehe nur einen Nachteil dieses Ansatzes: Wenn ein Test vor / nach dem Unterricht fehlschlägt, wird der Test nicht erneut ausgeführt.

user1459144
quelle
Beispiel in der Maven-Befehlszeile: mvn install -Dsurefire.rerunFailingTestsCount = 2
activout.se
18

Wie für mich schreiben benutzerdefinierte Läufer flexiblere Lösung. Die oben beschriebene Lösung (mit Codebeispiel) hat zwei Nachteile:

  1. Der Test wird nicht wiederholt, wenn er auf der @ BeforeClass-Stufe fehlschlägt.
  2. Die Berechnung von Tests läuft etwas anders ab (wenn Sie 3 Wiederholungsversuche haben, erhalten Sie Testläufe: 4, Erfolg 1, der möglicherweise verwirrend ist);

Deshalb bevorzuge ich mehr Ansatz beim Schreiben von benutzerdefinierten Läufern. Der Code des benutzerdefinierten Läufers könnte folgender sein:

import org.junit.Ignore;
import org.junit.internal.AssumptionViolatedException;
import org.junit.internal.runners.model.EachTestNotifier;
import org.junit.runner.Description;
import org.junit.runner.notification.RunNotifier;
import org.junit.runner.notification.StoppedByUserException;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;


public class RetryRunner extends BlockJUnit4ClassRunner {

    private final int retryCount = 100;
    private int failedAttempts = 0;

    public RetryRunner(Class<?> klass) throws InitializationError {
        super(klass);
    }    


    @Override
    public void run(final RunNotifier notifier) {
        EachTestNotifier testNotifier = new EachTestNotifier(notifier,
                getDescription());
        Statement statement = classBlock(notifier);
        try {

            statement.evaluate();
        } catch (AssumptionViolatedException e) {
            testNotifier.fireTestIgnored();
        } catch (StoppedByUserException e) {
            throw e;
        } catch (Throwable e) {
            retry(testNotifier, statement, e);
        }
    }

    @Override
    protected void runChild(final FrameworkMethod method, RunNotifier notifier) {
        Description description = describeChild(method);
        if (method.getAnnotation(Ignore.class) != null) {
            notifier.fireTestIgnored(description);
        } else {
            runTestUnit(methodBlock(method), description, notifier);
        }
    }

    /**
     * Runs a {@link Statement} that represents a leaf (aka atomic) test.
     */
    protected final void runTestUnit(Statement statement, Description description,
            RunNotifier notifier) {
        EachTestNotifier eachNotifier = new EachTestNotifier(notifier, description);
        eachNotifier.fireTestStarted();
        try {
            statement.evaluate();
        } catch (AssumptionViolatedException e) {
            eachNotifier.addFailedAssumption(e);
        } catch (Throwable e) {
            retry(eachNotifier, statement, e);
        } finally {
            eachNotifier.fireTestFinished();
        }
    }

    public void retry(EachTestNotifier notifier, Statement statement, Throwable currentThrowable) {
        Throwable caughtThrowable = currentThrowable;
        while (retryCount > failedAttempts) {
            try {
                statement.evaluate();
                return;
            } catch (Throwable t) {
                failedAttempts++;
                caughtThrowable = t;
            }
        }
        notifier.addFailure(caughtThrowable);
    }
}
user1459144
quelle
2
Problem ist, wenn der Test in der AfterClass-Methode fehlschlägt.
user1050755
1
Ich sehe kein Problem. Ich habe einen Beispieltest geschrieben, der einen Test mit dem angegebenen Läufer ausführt, und es scheint in Ordnung zu sein: @RunWith (RetryRunner.class) öffentliche Klasse TestSample {private static int i = 0; @AfterClass public static void testBefore () {System.out.println ("Vor dem Test"); i ++; if (i <2) {fail ("Fail"); }}}
user1459144
6

Sie müssen Ihre eigenen schreiben org.junit.runner.Runnerund Ihre Tests mit Anmerkungen versehen @RunWith(YourRunner.class).

CKuck
quelle
5

Der vorgeschlagene Kommentar wurde basierend auf diesem Artikel mit einigen Ergänzungen geschrieben.

Wenn ein Testfall aus Ihrem jUnit-Projekt das Ergebnis "Fehler" oder "Fehler" erhält, wird dieser Testfall noch einmal ausgeführt. Insgesamt haben wir hier 3 Chancen gesetzt, um ein Erfolgsergebnis zu erzielen.

Wir müssen also eine Regelklasse erstellen und Ihrer Testklasse "@Rule" -Nachrichten hinzufügen .

Wenn Sie nicht für jede Ihrer Testklassen dieselben "@ Rule" -Nachrichten schreiben möchten, können Sie diese zu Ihrer abstrakten SetProperty-Klasse hinzufügen (falls vorhanden) und von dieser erweitern.

Regelklasse:

import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;

public class RetryRule implements TestRule {
    private int retryCount;

    public RetryRule (int retryCount) {
        this.retryCount = retryCount;
    }

    public Statement apply(Statement base, Description description) {
        return statement(base, description);
    }

    private Statement statement(final Statement base, final Description description) {
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                Throwable caughtThrowable = null;

                // implement retry logic here
                for (int i = 0; i < retryCount; i++) {
                    try {
                        base.evaluate();
                        return;
                    } catch (Throwable t) {
                        caughtThrowable = t;
                        //  System.out.println(": run " + (i+1) + " failed");
                        System.err.println(description.getDisplayName() + ": run " + (i + 1) + " failed.");
                    }
                }
                System.err.println(description.getDisplayName() + ": giving up after " + retryCount + " failures.");
                throw caughtThrowable;
            }
        };
    }
}

Testklasse:

import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

/**
 * Created by ONUR BASKIRT on 27.03.2016.
 */
public class RetryRuleTest {

    static WebDriver driver;
    final private String URL = "http://www.swtestacademy.com";

    @BeforeClass
    public static void setupTest(){
        driver = new FirefoxDriver();
    }

    //Add this notification to your Test Class 
    @Rule
    public RetryRule retryRule = new RetryRule(3);

    @Test
    public void getURLExample() {
        //Go to www.swtestacademy.com
        driver.get(URL);

        //Check title is correct
        assertThat(driver.getTitle(), is("WRONG TITLE"));
    }
}
Sergii
quelle