¿Cómo volver a ejecutar las pruebas de JUnit fallidas inmediatamente?


Hay una manera de tener una Regla JUnit o algo similar que le da a cada prueba fallida una segunda oportunidad, simplemente tratando de ejecutarla una vez más.

Antecedentes: Tengo un gran conjunto de pruebas Selenium2-WebDriver escritas con JUnit. Debido a un tiempo muy agresivo (solo cortos períodos de espera después de los clics) algunas pruebas (1 de cada 100, y siempre una diferente) pueden fallar porque el servidor a veces responde un poco más lento. Pero no puedo hacer que el período de espera tan largo que es definitivamente lo suficiente, porque entonces las pruebas durarán para siempre.) think Así que creo que es aceptable para este caso de uso que una prueba es verde incluso si necesita un segundo intento.

Por supuesto, sería mejor tener una mayoría de 2 de 3 (repetir una prueba fallida 3 veces, y tomarlas como correctas, si dos de las pruebas son correctas), pero esto sería una mejora futura.

Author: Simon C, 2011-11-28

4 answers

Puedes hacer esto con un TestRule. Esto le dará la flexibilidad que necesita. Un TestRule le permite insertar lógica alrededor de la prueba, por lo que implementaría el bucle de reintento:

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

El corazón de a TestRule es el base.evaluate(), que llama a su método de prueba. Así que alrededor de esta llamada se pone un bucle de reintento. Si se lanza una excepción en su método de prueba (un error de aserción es en realidad un AssertionError), entonces la prueba ha fallado, y volverá a intentarlo.

Hay otra cosa que puede ser de uso. Es posible que solo desee aplicar esta lógica de reintento a un conjunto de pruebas, en cuyo caso puede agregar a la clase de Reintento por encima de una prueba para una anotación particular en el método. Description contiene una lista de anotaciones para el método. Para obtener más información sobre esto, consulte mi respuesta a ¿Cómo ejecutar un código antes de cada método de prueba JUnit @individualmente, sin usar @RunWith ni AOP?.

Usando un TestRunner personalizado

Esta es la sugerencia de CKuck, puede definir su propio Corredor. Necesitas extender BlockJUnit4ClassRunner y anular runChild(). Para obtener más información, consulte mi respuesta a ¿Cómo definir la regla del método JUnit en una suite?. Esta respuesta detalla cómo definir cómo ejecutar código para cada método en una suite, para lo cual debe definir su propio Runner.

 87
Author: Matthew Farwell,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:02:22

En cuanto a mí escribir corredor personalizado solución más flexible. La solución que se publicó anteriormente (con ejemplo de código) tiene dos desventajas:

  1. No volverá a intentar la prueba si falla en la etapa @BeforeClass;
  2. El cálculo de las pruebas se ejecuta un poco diferente (cuando tiene 3 reintentos, usted recibirá pruebas ejecutadas: 4, éxito 1 que podría ser confuso);

Es por eso que prefiero más enfoque con la escritura de corredor personalizado. Y el código de custom runner podría ser el siguiente:

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);
    }
}
 17
Author: user1459144,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-12-24 15:01:00

Ahora hay una mejor opción. Si estás usando plugins maven como: surfire o failsefe hay una opción para agregar parámetro rerunFailingTestsCount Api SurFire . Este material se implementó en el siguiente ticket: Jira Ticket. En este caso, no es necesario escribir su código personalizado y el plugin modifica automáticamente el informe de resultados de la prueba.
Veo solo un inconveniente de este enfoque: Si alguna prueba falla Antes/Después de la etapa de clase, la prueba no se volverá a ejecutar.

 16
Author: user1459144,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-05-29 10:08:22

Tienes que escribir tu propio org.junit.runner.Runner y anotar tus pruebas con @RunWith(YourRunner.class).

 7
Author: CKuck,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-11-28 12:36:05