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。