Jak natychmiast ponownie uruchomić nieudane testy JUnit?

81

Czy istnieje sposób, aby mieć regułę JUnit lub coś podobnego, co daje każdemu negatywnemu testowi drugą szansę, po prostu próbując uruchomić go jeszcze raz.

Kontekst: Mam duży zestaw testów Selenium2-WebDriver napisanych w JUnit. Z powodu bardzo agresywnego timingu (tylko krótkie okresy oczekiwania po kliknięciach) niektóre testy (1 na 100 i zawsze inny) mogą się nie powieść, ponieważ serwer czasami reaguje nieco wolniej. Ale nie mogę tak wydłużyć okresu oczekiwania, aby był na pewno wystarczająco długi, ponieważ wtedy testy potrwają wiecznie.) - Więc myślę, że w tym przypadku użycia jest dopuszczalne, aby test był zielony, nawet jeśli potrzebuje sekundy próbować.

Oczywiście lepiej byłoby mieć większość 2 z 3 (powtórzyć 3 razy negatywny test i przyjąć je jako poprawne, jeśli dwa testy są poprawne), ale byłaby to przyszła poprawa.

Ralph
źródło
1
Nie powinno być konieczne ustalenie czasu oczekiwania w selenie 2. WebDriver powinien wykryć ładowanie strony i odpowiednio poczekać. Jeśli chcesz poczekać na coś innego niż załadowanie strony, na przykład jakiś JavaScript do wykonania, powinieneś użyć klasy WebDriverWait, patrz: seleniumhq.org/docs/04_webdriver_advanced.html . To powiedziawszy, myślę, że może być w porządku powtórzenie testów GUI, chciałem tylko wyjaśnić, że w większości przypadków nie jest potrzebny wyraźny czas oczekiwania.
Tim Büthe
To prawda, ale zaznaczę również, że pracowałem na naprawdę, bardzo kiepskich serwerach, które są „w porządku”, ale mają NAPRAWDĘ długi czas działania w niektórych instancjach stron, dlatego nie chcę nie zdać. To świetne pytanie, dzięki. (naturalnie wolałbym, aby timing ZAWSZE był spójny i będziemy to naciskać, ale do tego czasu będzie to musiało wystarczyć)
cgp
Jeśli korzystasz z funkcji Cucumber rerun.txt, moją odpowiedź znajdziesz tutaj
Sugat Mankar
Jeśli używasz funkcji Cucumber rerun.txt, zobacz odpowiedzi tutaj.
Sugat Mankar

Odpowiedzi:

107

Możesz to zrobić za pomocą TestRule . Zapewni to elastyczność, której potrzebujesz. TestRule umożliwia wstawienie logiki wokół testu, więc zaimplementowałbyś pętlę ponawiania:

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");
    }
}

Sercem a TestRulejest base.evaluate(), który wywołuje Twoją metodę testową. Więc wokół tego wywołania umieszczasz pętlę ponawiania. Jeśli w metodzie testowej zostanie zgłoszony wyjątek (awaria potwierdzenia to w rzeczywistości an AssertionError), oznacza to, że test się nie powiódł i spróbujesz ponownie.

Jest jeszcze jedna rzecz, która może się przydać. Możesz chcieć zastosować tę logikę ponawiania tylko do zestawu testów, w takim przypadku możesz dodać do klasy Retry powyżej testu dla określonej adnotacji w metodzie. Descriptionzawiera listę adnotacji dla metody. Aby uzyskać więcej informacji na ten temat, zobacz moją odpowiedź na temat Jak uruchomić kod przed każdą metodą JUnit @Test indywidualnie, bez użycia @RunWith ani AOP? .

Korzystanie z niestandardowego narzędzia TestRunner

To jest sugestia CKucka, możesz zdefiniować własnego Runnera. Musisz rozszerzyć BlockJUnit4ClassRunner i nadpisać runChild (). Aby uzyskać więcej informacji, zobacz moją odpowiedź na temat Jak zdefiniować regułę metody JUnit w zestawie? . Ta odpowiedź zawiera szczegółowe informacje, jak zdefiniować sposób uruchamiania kodu dla każdej metody w pakiecie, dla którego musisz zdefiniować własnego Runnera.

Matthew Farwell
źródło
Dzięki: BTW dla każdego, kto spróbuje tego, TestRule to funkcja, która istnieje od wersji JUnit 4.9
Ralph
@Ralph Właściwie, TestRule jest zamiennikiem MethodRule, który został wprowadzony wcześniej, około 4.7 IIRC, więc to rozwiązanie może potencjalnie mieć zastosowanie przed 4.9, ale byłoby nieco inne.
Matthew Farwell
7
To było naprawdę pomocne, ale coś, co przyszło mi do głowy: retryCount i ponowne próby mogą być mylącymi nazwami. Gdy ponowna próba wynosi 1, zakładam, że uruchamia test, a jeśli się nie powiedzie, ponawia próbę raz, ale tak nie jest. Zmienna prawdopodobnie powinna nazywać się maxTries.
Thomas M.
1
@MatthewFarwell: czy to powoduje ponowne uruchomienie działania? Czy jest jakiś sposób, żebyśmy mogli to zrobić?
Basim Sherif
4
Korzystanie z tej metody wiąże się z ograniczeniem polegającym na tym, że powtórzenia testów są wykonywane bez ponownego tworzenia instancji testowej. Oznacza to, że żadne pola instancji w klasie testowej (lub superklasach) nie zostaną ponownie zainicjowane, prawdopodobnie pozostawiając stan z wcześniejszych uruchomień.
Jonah Graham
19

Teraz jest lepsza opcja. Jeśli używasz wtyczek maven takich jak: surfire lub failsefe, istnieje możliwość dodania parametru rerunFailingTestsCount SurFire Api . Te rzeczy zostały zaimplementowane w następującym bilecie: Jira Ticket . W takim przypadku nie musisz pisać własnego kodu, a wtyczka automatycznie zmienia raport wyników testu.
Widzę tylko jedną wadę tego podejścia: jeśli jakiś test nie powiedzie się na etapie przed / po zajęciach, test nie będzie powtórzony.

user1459144
źródło
Przykład w wierszu poleceń Maven: mvn install -Dsurefire.rerunFailingTestsCount = 2
activout.se
18

Jak dla mnie pisząc niestandardowy biegacz bardziej elastyczne rozwiązanie. Powyższe rozwiązanie (z przykładem kodu) ma dwie wady:

  1. Nie będzie ponawiać próby, jeśli nie powiedzie się na etapie @BeforeClass;
  2. Obliczanie testów przebiega nieco inaczej (gdy masz 3 powtórzenia, otrzymasz Uruchomienia testowe: 4, sukces 1, który może być mylący);

Dlatego wolę bardziej podejście do pisania niestandardowego runnera. A kod niestandardowego runnera może wyglądać następująco:

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
źródło
2
Problem występuje, gdy test kończy się niepowodzeniem w metodzie AfterClass.
user1050755
1
Nie widzę żadnego problemu. Napisałem przykładowy test, który uruchamia test z określonym runnerem i wydaje się, że działa dobrze: @RunWith (RetryRunner.class) klasa publiczna TestSample {private static int i = 0; @AfterClass public static void testBefore () {System.out.println ("Przed testem"); i ++; if (i <2) {fail ("Fail"); }}}
user1459144
6

Musisz napisać własne org.junit.runner.Runneri opatrzyć adnotacje @RunWith(YourRunner.class).

CKuck
źródło
5

Proponowany komentarz został napisany na podstawie tego artykułu z pewnymi dodatkami.

W tym przypadku, jeśli jakiś przypadek testowy z projektu jUnit otrzyma wynik „niepowodzenie” lub „błąd”, ten przypadek testowy zostanie ponownie uruchomiony jeszcze raz. Całkowicie tutaj ustawiliśmy 3 szanse na uzyskanie wyniku sukcesu.

Dlatego musimy utworzyć klasę reguł i dodać powiadomienia „@Rule” do Twojej klasy testowej .

Jeśli nie chcesz tworzyć tych samych powiadomień „@Rule” dla każdej klasy testowej, możesz dodać ją do swojej abstrakcyjnej klasy SetProperty (jeśli ją masz) i rozszerzyć ją.

Klasa reguły:

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;
            }
        };
    }
}

Klasa testowa:

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
źródło
0

Ta odpowiedź jest zbudowana na tej odpowiedzi .

Jeśli chcesz, aby Twoja ActivityScenario(i Twoja Aktywność) były odtwarzane przed każdym biegiem, możesz je uruchomić za pomocą try-with-resources. ActivityScenarioZostanie zamknięte automatycznie po każdej próbie.

public final class RetryRule<A extends Activity> implements TestRule {
    private final int retryCount;
    private final Class<A> activityClazz;
    private ActivityScenario<A> scenario;

    /**
     * @param retryCount the number of retries. retryCount = 1 means 1 (normal) try and then
     * 1 retry, i.e. 2 tries overall
     */
    public RetryRule(int retryCount, @NonNull Class<A> clazz) {
        this.retryCount = retryCount;
        this.activityClazz = clazz;
    }

    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(ActivityScenario<A> scenario = ActivityScenario.launch(activityClazz)){
                        RetryRule.this.scenario = scenario;
                        base.evaluate();
                        return;
                    } catch (Throwable t) {
                        caughtThrowable = t;
                        Log.e(LOGTAG,
                                description.getDisplayName() + ": run " + (i + 1) + " failed: ", t);
                    }
                }
                Log.e(LOGTAG,
                        description.getDisplayName() + ": giving up after " + (retryCount + 1) +
                                " failures");
                throw Objects.requireNonNull(caughtThrowable);
            }
        };
    }

    public ActivityScenario<A> getScenario() {
        return scenario;
    }
}

Następnie możesz uzyskać dostęp do swojego scenariusza w testach za pomocą getScenario()metody.

DanD
źródło