如何立即重新运行失败的JUnit测试?


81

有没有办法让JUnit规则或类似的东西通过尝试再次运行它,使每一次失败的测试都有第二次机会。

背景:我有大量用JUnit编写的Selenium2-WebDriver测试集。由于时间安排非常激进(单击后仅等待很短的时间),某些测试(100个测试中的1个,总是一个不同的测试)可能会失败,因为服务器有时响应速度会变慢。但是我不能将等待期定得足够长,因为它肯定足够长,因为这样测试将永远进行下去。)-因此,对于这种用例,我认为可接受的测试是绿色的,即使它需要一秒钟尝试。

当然,最好是三分之二(如果失败的测试重复3次,并且如果其中两个测试是正确的,则将它们视为正确),但这将是未来的改进。


1
硒2中不必有固定的等待时间。WebDriver应该检测页面加载并相应地等待。如果要等待页面加载之外的其他事情(例如要执行的JavaScript),则应使用WebDriverWait类,请参阅:seleniumhq.org/docs/04_webdriver_advanced.html。就是说,我认为重试GUI测试可能还可以,我只是想澄清一下,在大多数情况下不需要明确的等待时间。
蒂姆·布斯(TimBüthe)

这是真的,但我还可以指出的是,我已经有些真的,真的很可怜服务器是“精”的工作,但他们有一个真正长的自旋时间上的某些页面实例,因此,我不希望失败。谢谢,这是一个很好的问题。(自然,我希望时间总是保持一致,我们将为此努力,但是在那之前,这必须做到)
cgp 2012年

如果您使用的是黄瓜rerun.txt功能,请在这里
Sugat Mankar,2015年

如果您正在使用Cucumber rerun.txt功能,请在此处查看。
Sugat Mankar,2015年

Answers:


105

您可以使用TestRule进行此操作。这将为您提供所需的灵活性。TestRule允许您在测试周围插入逻辑,因此可以实现重试循环:

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

a的心脏TestRulebase.evaluate(),它调用您的测试方法。因此,围绕此调用,您放置了一个重试循环。如果您的测试方法中引发了异常(断言失败实际上是AssertionError),则测试失败,然后您将重试。

还有另一件事可能有用。您可能只想将此重试逻辑应用于一组测试,在这种情况下,您可以在测试上方的Retry类中添加方法上的特定注释。Description包含该方法的注释列表。有关此的更多信息,请参见我的答案,如何在不使用@RunWith或AOP的情况下分别在每个JUnit @Test方法之前运行一些代码?

使用自定义TestRunner

这是CKuck的建议,您可以定义自己的Runner。您需要扩展BlockJUnit4ClassRunner并覆盖runChild()。有关更多信息,请参见我对如何在套件中定义JUnit方法规则的回答。该答案详细说明了如何为Suite中的每个方法定义如何运行代码,为此您必须定义自己的Runner。


谢谢:顺便说一句,每个人都会尝试这种方法,TestRule是自JUnit 4.9版以来一直存在的功能
Ralph

@Ralph实际上,TestRule替代了之前引入的MethodRule,大约是4.7 IIRC,因此该解决方案可以在4.9之前应用,但会略有不同。
马修·法威尔

7
这确实很有帮助,但是让我大吃一惊的是:retryCount和retry可能会误导名称。当重试为1时,我认为他将运行测试,如果失败,则重试一次,但事实并非如此。该变量可能应该称为maxTries。
Thomas M.

1
@MatthewFarwell:这会重启活动吗?有什么办法,我们可以做到吗?
Basim Sherif

4
使用此方法确实有一个约束,即在不重新创建测试实例的情况下完成了测试重新运行。这意味着测试类(或超类)中的任何实例字段都不会重新初始化,可能会保留之前运行时的状态。
乔纳·格雷厄姆

19

现在有一个更好的选择。如果您使用的是maven插件:surfire或failefe,则可以选择添加参数rerunFailingTestsCount SurFire Api。在以下票证中实现了这些东西:Jira Ticket。在这种情况下,您无需编写自定义代码,插件会自动修改测试结果报告。
我只看到这种方法的一个缺点:如果某项测试在课前/课后失败,则不会重新运行。


Maven命令行上的示例:mvn install -Dsurefire.rerunFailingTestsCount = 2
activout.se

18

对于我来说,编写自定义运行器更灵活的解决方案。上面发布的解决方案(带有代码示例)有两个缺点:

  1. 如果在@BeforeClass阶段失败,它将不重试测试;
  2. 它计算测试的运行方式略有不同(当您进行3次重试时,您将收到测试运行:4,成功1可能会令人困惑);

这就是为什么我更喜欢编写自定义运行器的方法。自定义运行程序的代码可能如下:

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

2
问题是AfterClass方法中的测试失败。
user1050755 2014年

1
我没看到任何问题。我编写了示例测试,该测试使用指定的运行程序运行测试,并且看起来可以正常工作:@RunWith(RetryRunner.class)公共类TestSample {private static int i = 0; @AfterClass公共静态无效testBefore(){System.out.println(“测试前”); i ++; if(i <2){失败(“失败”);}
user1459144 2014年

6

您必须自己编写org.junit.runner.Runner并使用注释测试@RunWith(YourRunner.class)


5

建议的评论是根据这篇文章撰写的,并附有一些补充内容。

在这里,如果jUnit项目中的某些测试用例得到“失败”或“错误”的结果,则该测试用例将再次运行一次。在这里,我们总共设置了3个获得成功结果的机会。

所以,我们需要创建规则类添加“@rule”通知到您的测试类

如果不想为每个测试类使用相同的“ @Rule”通知,则可以将其添加到抽象SetProperty类(如果有)中并从中扩展。

规则类别:

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

测试类别:

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

0

此答案基于此答案

如果需要ActivityScenario在每次运行之前重新创建您的(和您的活动),则可以使用try-with-resources启动它。在ActivityScenario随后将每次尝试后自动关闭。

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

然后,您可以使用该getScenario()方法在测试中访问您的方案。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.