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

81

¿Hay alguna forma de tener una regla JUnit o algo similar que le dé a cada prueba que falla una segunda oportunidad, simplemente intentando ejecutarla una vez más?

Antecedentes: tengo un gran conjunto de pruebas de Selenium2-WebDriver escritas con JUnit. Debido a una sincronización muy agresiva (solo breves 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 sea tan largo que definitivamente sea lo suficientemente largo, porque entonces las pruebas durarán una eternidad.) - Así que creo que es aceptable para este caso de uso que una prueba sea verde incluso si necesita un segundo tratar.

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

Ralph
fuente
1
No debería ser necesario tener tiempos de espera fijos en selenium 2. El WebDriver debería detectar la carga de la página y esperar en consecuencia. Si desea esperar a que se cargue la página, por ejemplo, algo de JavaScript para ejecutar, debe usar la clase WebDriverWait, consulte: seleniumhq.org/docs/04_webdriver_advanced.html . Dicho esto, creo que puede estar bien volver a intentar las pruebas de GUI, solo quería aclarar que en la mayoría de los casos no se necesita un tiempo de espera explícito.
Tim Büthe
Es cierto, pero también señalaré que he trabajado en algunos servidores realmente pobres que están "bien", pero tienen un tiempo de rotación REALMENTE largo en ciertas instancias de página y, por lo tanto, no quiero fallar. Esta es una gran pregunta, gracias. (naturalmente, preferiría que el tiempo SIEMPRE sea ​​consistente, y vamos a presionar por eso, pero hasta entonces, esto tendrá que ser suficiente)
cgp
Si está utilizando la función pepino rerun.txt, encuentre mi respuesta aquí
Sugat Mankar
Si está utilizando la función Cucumber rerun.txt, consulte ans aquí.
Sugat Mankar

Respuestas:

105

Puede hacer esto con TestRule . Esto le dará la flexibilidad que necesita. Una TestRule le permite insertar lógica alrededor de la prueba, por lo que implementaría el ciclo 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 TestRulees el base.evaluate(), que llama a su método de prueba. Entonces, alrededor de esta llamada, pones 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 útil. Es posible que solo desee aplicar esta lógica de reintento a un conjunto de pruebas, en cuyo caso puede agregar a la clase Reintentar encima de una prueba para una anotación particular en el método. Descriptioncontiene 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 JUnit @Test individualmente, sin usar @RunWith ni AOP? .

Usando un TestRunner personalizado

Esta es la sugerencia de CKuck, puedes definir tu propio Runner. Necesita 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.

Matthew Farwell
fuente
Gracias: Por cierto, para todos cómo intentarán esto, TestRule es una característica que existe desde la versión 4.9 de JUnit
Ralph
@Ralph En realidad, TestRule es un reemplazo de MethodRule, que se introdujo anteriormente, alrededor de 4.7 IIRC, por lo que esta solución se puede aplicar potencialmente antes de 4.9, pero sería ligeramente diferente.
Matthew Farwell
7
Esto fue realmente útil, pero algo que me vino a la mente: retryCount y los reintentos pueden ser nombres engañosos. Cuando reintentar es 1, supongo que ejecuta la prueba y, si falla, vuelve a intentarlo una vez, pero ese no es el caso. La variable probablemente debería llamarse maxTries.
Thomas M.
1
@MatthewFarwell: ¿esto reinicia la actividad? ¿Hay alguna forma de que podamos hacer eso?
Basim Sherif
4
El uso de este método tiene la restricción de que las repeticiones de la prueba se realizan sin volver a crear la instancia de prueba. Eso significa que los campos de instancia en la clase de prueba (o superclases) no se reinicializarán, posiblemente dejando el estado de las ejecuciones anteriores.
Jonah Graham
19

Ahora hay una mejor opción. Si está utilizando complementos de maven como: surfire o failsefe, hay una opción para agregar el parámetro rerunFailingTestsCount SurFire Api . Esto se implementó en el siguiente ticket: Jira Ticket . En este caso, no es necesario que escriba su código personalizado y el complemento modifique automáticamente el informe de resultados de la prueba.
Solo veo un inconveniente de este enfoque: si alguna prueba falla en la etapa de antes / después de la clase, la prueba no se volverá a ejecutar.

usuario1459144
fuente
Ejemplo en la línea de comandos de Maven: mvn install -Dsurefire.rerunFailingTestsCount = 2
activout.se
18

En cuanto a mí, escribir un corredor personalizado, una 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. Las pruebas de cálculo se ejecutan de manera un poco diferente (cuando tiene 3 reintentos, recibirá Ejecuciones de prueba: 4, éxito 1 que puede ser confuso);

Es por eso que prefiero un enfoque más al escribir un corredor personalizado. Y el código del corredor personalizado podría estar siguiendo:

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);
    }
}
usuario1459144
fuente
2
El problema es cuando la prueba falla en el método AfterClass.
user1050755
1
No veo ningún problema. Escribí una prueba de muestra que ejecuta la prueba con un corredor especificado y parece que funciona bien: @RunWith (RetryRunner.class) public class TestSample {private static int i = 0; @AfterClass public static void testBefore () {System.out.println ("Antes de la prueba"); i ++; if (i <2) {fail ("Fail"); }}}
usuario1459144
6

Tienes que escribir el tuyo org.junit.runner.Runnery anotar tus pruebas con @RunWith(YourRunner.class).

CKuck
fuente
5

El comentario propuesto se escribió en base a este artículo con algunas adiciones.

Aquí, si algún caso de prueba de su proyecto jUnit obtiene un resultado de "falla" o "error", este caso de prueba se volverá a ejecutar una vez más. Totalmente aquí establecemos 3 posibilidades de obtener un resultado exitoso.

Por lo tanto, necesitamos crear una clase de reglas y agregar notificaciones "@Rule" a su clase de prueba .

Si no desea escribir las mismas notificaciones de "@Rule" para cada clase de prueba, puede agregarla a su clase SetProperty abstracta (si la tiene) y extenderla.

Clase de regla:

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

Clase de prueba:

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
fuente
0

Esta respuesta se basa en esta respuesta .

Si necesita que su ActivityScenario(y su Actividad) se vuelva a crear antes de cada ejecución, puede iniciarla utilizando try-with-resources. A ActivityScenariocontinuación, se cerrará automáticamente después de cada intento.

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

Luego puede acceder a su escenario en sus pruebas utilizando el getScenario()método.

DanD
fuente