86 votes

Comment Ré-exécuter échoué JUnit tests immédiatement?

Est-il un moyen d'avoir un JUnit Règle ou quelque chose de semblable qui donne à chaque test en échec une seconde chance, en essayant simplement de l'exécuter une fois de plus.

Contexte: j'ai un grand Ensemble de Selenium2-WebDriver tests écrits avec JUnit. En raison d'une très agressif timing (seulement sur de courtes périodes d'attente après la clique) certains tests (1 sur 100, et toujours un autre) peut échouer parce que le serveur répond parfois un peu plus lent. Mais je ne peux pas faire la période d'attente si longue qu'elle est certainement assez longtemps, car alors les épreuves pour toujours.) -- Je pense donc qu'il est acceptable pour ce cas d'utilisation d'un test est vert, même si elle a besoin d'un deuxième essai.

Bien sûr, il serait préférable d'avoir un 2 de 3 la majorité (répétition de l'échec de test 3 fois, et de les prendre comme correcte, si deux de ces tests sont corrects), mais ce serait une amélioration future.

112voto

Matthew Farwell Points 31257

Vous pouvez le faire avec un TestRule. Cela vous donnera la souplesse dont vous avez besoin. Un TestRule vous permet d'insérer de la logique de l'essai, de sorte que vous pouvez implémenter 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 le base.evaluate(), qui appelle la méthode de test. Donc autour de cet appel vous mettre 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 devrez recommencer.

Il y a une autre chose qui peut être utile. Vous ne voulez appliquer cette logique de nouvelle tentative à une série de tests, dans ce cas, vous pouvez ajouter dans la nouvelle tentative de la classe au-dessus d'un test pour une annotation sur la méthode. Description contient une liste d'annotations pour la méthode. Pour plus d'informations à ce sujet, voir ma réponse à la Façon d'exécuter du code avant chaque JUnit @la méthode d'Essai individuellement, sans utiliser l'annotation @RunWith ni AOP?.

À l'aide d'un Lanceur personnalisé

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

20voto

user1459144 Points 374

Comme pour moi, l'écriture personnalisée coureur solution plus flexible. La solution qui a posté ci-dessus (avec un exemple de code) a deux inconvénients:

  1. Il ne réessaie pas de test si elle échoue à la @BeforeClass de la scène;
  2. Calcul de tests de fonctionner un peu différemment (quand vous avez 3 tentatives, vous recevrez essais: 4, succès 1 qui pourrait être source de confusion);

C'est pourquoi je préfère plus l'approche de l'écriture personnalisée coureur. Et le code de la coutume coureur pourraient être les suivants:

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

6voto

CKuck Points 324

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X