1. 概述

自动化测试是现代软件开发的核心环节,但并非所有测试失败都由代码缺陷导致。某些失败具有偶发性,可能由竞态条件、网络延迟或资源限制等外部因素引发。

针对这类瞬时性失败,为测试添加重试机制是稳定测试套件的利器。重试逻辑允许测试在被判定为失败前自动重新运行指定次数,这对提升 CI 管道稳定性、减少误报至关重要。

本文将深入探讨 在 JUnit 4 和 JUnit 5 中实现测试重试,涵盖自定义实现和基于库的解决方案,并总结最佳实践。

2. JUnit 5 重试机制实现

JUnit 5 的强大扩展模型相比 JUnit 4 更易定制测试行为。实现重试逻辑有两种主流方案:

  • 自定义扩展:通过编程方式处理测试失败重试
  • 使用 JUnit Pioneer 等库:直接使用现成重试注解

2.1. 基于 TestExecutionExceptionHandler 的自定义扩展

通过实现 TestExecutionExceptionHandler 接口,我们可以在测试抛出异常时触发重试:

public class RetryExtension implements TestExecutionExceptionHandler {
    private static final int MAX_RETRIES = 3;
    private static final ExtensionContext.Namespace NAMESPACE =
      ExtensionContext.Namespace.create("RetryExtension");

    @Override
    public void handleTestExecutionException(ExtensionContext context, Throwable throwable) throws Throwable {
        Store store = context.getStore(NAMESPACE);
        int retries = store.getOrDefault("retries", Integer.class, 0);

        if (retries < MAX_RETRIES) {
            retries++;
            store.put("retries", retries);
            System.out.println("Retrying test " + context.getDisplayName() + ", attempt " + retries);
            throw throwable;
        } else {
            throw throwable;
        }
    }
}

实际使用时,只需在测试类添加 @ExtendWith 注解:

@ExtendWith(RetryExtension.class)
public class RetryTest {
    private static int attempt = 0;

    @Test
    public void testWithRetry() {
        attempt++;
        System.out.println("Test attempt: " + attempt);

        if (attempt < 3) {
            throw new RuntimeException("Failing test");
        }
    }
}

核心机制

  • 通过 TestExecutionExceptionHandler 拦截测试异常
  • 使用 ExtensionContext.Store 跟踪重试次数
  • 达到最大重试次数后抛出异常标记失败

2.2. 使用 JUnit Pioneer 的 @RetryingTest

更简单的方案是直接使用 JUnit Pioneer 库提供的 @RetryingTest 注解:

首先添加依赖(Maven 示例):

<dependency>
    <groupId>org.junit-pioneer</groupId>
    <artifactId>junit-pioneer</artifactId>
    <version>2.0.1</version>
    <scope>test</scope>
</dependency>

测试代码示例:

public class RetryPioneerTest {
    private static int attempt = 0;

    @RetryingTest(maxAttempts = 3)
    void testWithRetry() {
        attempt++;
        System.out.println("Test attempt: " + attempt);

        if (attempt < 3) {
            throw new RuntimeException("Failing test");
        }
    }
}

⚠️ 注意@RetryingTest 会替换 @Test 注解,无需同时使用。只需指定最大重试次数,库会自动处理重试逻辑。

3. JUnit 4 重试机制实现

JUnit 4 虽然缺乏现代扩展模型,但可通过自定义 TestRule 实现重试:

public class RetryRule implements TestRule {
    private final int retryCount;

    public RetryRule(int retryCount) {
        this.retryCount = retryCount;
    }

    @Override
    public Statement apply(Statement base, Description description) {
        return new Statement() {
            @Override
            public void evaluate() throws Throwable {
                Throwable failure = null;

                for (int i = 0; i < retryCount; i++) {
                    try {
                        base.evaluate();
                        return;
                    } catch (Throwable t) {
                        failure = t;
                        System.out.println("Retry " + (i + 1) + "/" + retryCount +
                          " for test " + description.getDisplayName());
                    }
                }

                throw failure;
            }
        };
    }
}

使用方式:

public class RetryRuleTest {
    @Rule
    public RetryRule retryRule = new RetryRule(3);

    private static int attempt = 0;

    @Test
    public void testWithRetry() {
        attempt++;
        System.out.println("Test attempt: " + attempt);

        if (attempt < 3) {
            throw new RuntimeException("Failing test");
        }
    }
}

踩坑点:JUnit 4 的 TestRule 实现需注意:

  • 重试计数器需使用静态变量(每个测试方法独立)
  • 若所有重试均失败,必须抛出最后捕获的异常

4. 测试重试最佳实践

使用重试机制时需遵循以下原则:

  • 适用场景:仅对偶发性失败(如时序问题、环境抖动)使用重试,切勿用于掩盖持续性缺陷
  • 日志记录:每次重试必须打印日志,便于调试(如 Retrying test #attempt 2/3
  • ⚠️ 重试次数:默认 2-3 次为宜,过多重试会拖慢执行速度并掩盖问题
  • ⚠️ 临时方案:重试是治标不治本的方案,应优先修复根本原因

5. 总结

测试重试机制能显著提升测试套件可靠性,尤其在偶发性失败频发的 CI 环境中。JUnit 4/5 均支持通过自定义实现或第三方库(如 JUnit Pioneer)添加重试能力。

切记:重试是应对外部环境波动的工具,而非忽略缺陷的手段。合理使用重试,持续构建高质量软件。本文示例代码已上传至 GitHub


原始标题:How to Implement Retry for JUnit Tests | Baeldung