web-dev-qa-db-fra.com

Comment relancer immédiatement les tests JUnit ayant échoué?

Existe-t-il un moyen d'avoir une règle JUnit ou quelque chose de similaire qui donne à chaque test échoué une seconde chance, juste en essayant de l'exécuter à nouveau.

Contexte: J'ai un grand ensemble de tests Selenium2-WebDriver écrits avec JUnit. En raison d'un timing très agressif (seulement de courtes périodes d'attente après les clics), certains tests (1 sur 100, et toujours différent) peuvent échouer parce que le serveur répond parfois un peu plus lentement. Mais je ne peux pas rendre la période d'attente si longue qu'elle est définitivement assez longue, car alors les tests prendront pour toujours.) - Je pense donc qu'il est acceptable pour ce cas d'utilisation que un test est vert même s'il a besoin d'un deuxième essai.

Bien sûr, il serait préférable d'avoir une majorité de 2 sur 3 (répéter un test qui échoue 3 fois et les prendre comme corrects si deux des tests sont corrects), mais ce serait une amélioration future.

76
Ralph

Vous pouvez le faire avec un TestRule . Cela vous donnera la flexibilité dont vous avez besoin. Un TestRule vous permet d'insérer une logique autour du test, vous implémenteriez donc la boucle de nouvelle tentative:

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

Le cœur d'un TestRule est la base.evaluate(), qui appelle votre méthode de test. Donc, autour de cet appel, vous mettez une boucle de nouvelle tentative. Si une exception est levée dans votre méthode de test (un échec d'assertion est en fait un AssertionError), alors le test a échoué et vous réessayez.

Il y a une autre chose qui peut être utile. Vous pouvez uniquement appliquer cette logique de nouvelle tentative à un ensemble de tests, auquel cas vous pouvez ajouter à la classe Réessayer au-dessus d'un test pour une annotation particulière sur la méthode. Description contient une liste d'annotations pour la méthode. Pour plus d'informations à ce sujet, voir ma réponse à Comment exécuter du code avant chaque méthode JUnit @Test individuellement, sans utiliser @RunWith ni AOP? .

Utilisation d'un TestRunner personnalisé

C'est la suggestion de CKuck, vous pouvez définir votre propre Runner. Vous devez étendre BlockJUnit4ClassRunner et remplacer runChild (). Pour plus d'informations, voir ma réponse à Comment définir la règle de la méthode JUnit dans une suite? . Cette réponse détaille comment définir comment exécuter du code pour chaque méthode dans une suite, pour laquelle vous devez définir votre propre Runner.

99
Matthew Farwell

Quant à moi, l'écriture de coureur personnalisé solution plus flexible. La solution publiée ci-dessus (avec un exemple de code) présente deux inconvénients:

  1. Il ne réessayera pas le test s'il échoue à l'étape @BeforeClass;
  2. Il calcule les tests qui s'exécutent un peu différemment (lorsque vous avez 3 tentatives, vous recevrez des tests: 4, succès 1 qui peut être déroutant);

C'est pourquoi je préfère plus d'approche avec l'écriture de runner personnalisé. Et le code du coureur personnalisé pourrait être le suivant:

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);
    }
}
18
user1459144

Maintenant, il y a une meilleure option. Si vous utilisez des plugins maven comme: surfire ou failsefe, il y a une option pour ajouter le paramètre rerunFailingTestsCountSurFire Api . Ce truc a été implémenté dans le ticket suivant: Jira Ticket . Dans ce cas, vous n'avez pas besoin d'écrire votre code personnalisé et le plugin modifie automatiquement le rapport des résultats des tests.
Je ne vois qu'un inconvénient de cette approche: si certains tests échouent avant/après, le test de niveau de classe ne sera pas réexécuté.

17
user1459144

Vous devez écrire votre propre org.junit.runner.Runner Et annoter vos tests avec @RunWith(YourRunner.class).

6
CKuck

Le commentaire proposé a été rédigé sur la base d'un article ob this avec quelques ajouts.

Ici, si un scénario de test de votre projet jUnit obtient un résultat "échec" ou "erreur", ce scénario de test sera réexécuté une fois de plus. Totalement ici, nous avons défini 3 chances d'obtenir un résultat réussi.

Nous devons donc créer une classe de règles et ajouter des notifications "@Rule" à votre classe de test .

Si vous ne souhaitez pas utiliser les mêmes notifications "@Rule" pour chacune de vos classes de test, vous pouvez les ajouter à votre classe SetProperty abstraite (si vous les avez) et les étendre.

Classe de règles:

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

Classe de test:

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