【问题标题】:How to Re-run failed JUnit tests immediately?如何立即重新运行失败的 JUnit 测试?
【发布时间】:2012-01-07 20:50:18
【问题描述】:

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

背景:我有大量用 JUnit 编写的 Selenium2-WebDriver 测试。由于非常激进的时间安排(点击后只有很短的等待时间),一些测试(100 次中的 1 次,并且总是不同的测试)可能会失败,因为服务器有时会响应较慢。但是我不能让等待时间太长以至于它绝对足够长,因为那样测试将永远持续。) - 所以我认为对于这个用例来说,即使需要第二个测试也是绿色的也是可以接受的试试看。

当然最好有 3 票中的 2 票(重复失败的测试 3 次,如果其中两个测试正确,则认为它们是正确的),但这将是未来的改进。

【问题讨论】:

  • 在 selenium 2 中没有必要修复等待时间。WebDriver 应该检测页面加载并相应地等待。如果您想等待页面加载以外的其他内容,例如执行一些 JavaScript,您应该使用 WebDriverWait 类,请参阅:seleniumhq.org/docs/04_webdriver_advanced.html。也就是说,我认为重试 GUI 测试可能没问题,我只是想澄清一下,在大多数情况下不需要明确的等待时间。
  • 确实如此,但我还要指出,我曾在一些非常非常糟糕的服务器上工作过,这些服务器“很好”,但它们有一个 REALLY 长时间的旋转某些页面实例的正常运行时间,因此,我不想失败。这是一个很好的问题,谢谢。 (当然,我希望时间始终保持一致,我们将推动这一点,但在那之前,这将不得不这样做)
  • 如果您正在使用黄瓜 rerun.txt 功能,请找到我的答案here
  • 如果您使用 Cucumber rerun.txt 功能,请参阅 ans here.

标签: java testing junit


【解决方案1】:

此答案基于this answer

如果您需要在每次运行之前重新创建您的 ActivityScenario(和您的 Activity),您可以使用 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() 方法在测试中访问您的场景。

【讨论】:

    【解决方案2】:

    建议的评论是基于 ob this 文章编写的,并添加了一些内容。

    在这里,如果您的 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"));
        }
    }
    

    【讨论】:

      【解决方案3】:

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

      【讨论】:

      • Maven 命令行示例:mvn install -Dsurefire.rerunFailingTestsCount=2
      【解决方案4】:

      您可以使用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");
          }
      }
      

      TestRule 的核心是 base.evaluate(),它调用您的测试方法。所以围绕这个调用你放了一个重试循环。如果您的测试方法中抛出异常(断言失败实际上是AssertionError),则测试失败,您将重试。

      还有另一件事可能有用。您可能只想将此重试逻辑应用于一组测试,在这种情况下,您可以在 Retry 类中为方法上的特定注释添加测试上方的测试。 Description 包含该方法的注释列表。有关这方面的更多信息,请参阅我对How to run some code before each JUnit @Test method individually, without using @RunWith nor AOP? 的回复。

      使用自定义 TestRunner

      这是CKuck的建议,你可以定义自己的Runner。您需要扩展 BlockJUnit4ClassRunner 并覆盖 runChild()。有关更多信息,请参阅我对How to define JUnit method rule in a suite? 的回答。这个答案详细说明了如何定义如何为套件中的每个方法运行代码,您必须为此定义自己的 Runner。

      【讨论】:

      • 谢谢:顺便说一句,每个人都将如何尝试,TestRule 是自 JUnit 4.9 版以来存在的一项功能
      • @Ralph 实际上,TestRule 是 MethodRule 的替代品,MethodRule 是早先介绍的,大约 4.7 IIRC,因此该解决方案可能适用于 4.9 之前,但会略有不同。
      • 这确实很有帮助,但让我大吃一惊:retryCount 和 retries 可能是误导性名称。当重试为 1 时,我会假设他运行测试,如果失败,重试一次,但事实并非如此。该变量可能应该称为 maxTries。
      • @MatthewFarwell:这会重新启动活动吗?有什么办法,我们可以做到吗?
      • 使用此方法确实有一个约束,即在不重新创建测试实例的情况下完成测试重新运行。这意味着测试类(或超类)中的任何实例字段都不会重新初始化,可能会离开早期运行的状态。
      【解决方案5】:

      至于我编写自定义跑步者更灵活的解决方案。 上面发布的解决方案(带有代码示例)有两个缺点:

      1. 如果在@BeforeClass 阶段失败则不会重试;
      2. 计算测试的运行方式略有不同(当您重试 3 次时, 您将收到 test Runs: 4, success 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);
          }
      }
      

      【讨论】:

      • 问题是在 AfterClass 方法中测试失败。
      • 我没有发现任何问题。我编写了使用指定运行器运行测试的示例测试,它似乎工作正常:@RunWith(RetryRunner.class) public class TestSample { private static int i = 0; @AfterClass public static void testBefore() { System.out.println("Before test");我++; if(i
      【解决方案6】:

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

      【讨论】:

        猜你喜欢
        • 2013-06-08
        • 2011-12-05
        • 2013-05-20
        • 2018-07-26
        • 2019-04-15
        • 1970-01-01
        • 1970-01-01
        • 2020-09-19
        • 2023-02-15
        相关资源
        最近更新 更多