我正在使用junit 5运行测试,偶尔会挂起一个测试。通常这是在云服务的驱动程序中,但我们还没有发现它,所以我们不知道确切在哪里。我想将junit配置为在超时发生时输出所有线程的堆栈跟踪。但我看不出这是怎么容易做到的。当然,我可以构建自己的超时机制,并将其安装在@BeforeEach中,然后在@AfterEach中取消它,打印线程堆栈转储也很容易,但如果junit内置了类似";在超时时执行这个lambda";。我在assertTimeout*((函数中没有看到类似的内容。
更新:Junit5扩展是我能找到的最好的解决方案,看看我的答案。
很多评论都在暗示推出自己的解决方案,最终我不得不这样做。这真的不难,因为我的一位同事写了Junit 5扩展,我能够利用他的专业知识,我很高兴与社区分享。
这个扩展涉及两件事:
- 扩展类
- 可以添加到测试中的注释
一旦你在测试的类路径中有了这两件事,你就可以注释你的测试类(或测试类的基类(,比如:
@ExtendWith({TimeoutExtension.class})
public class Test1Timeout {
这将启用扩展。然后用注释测试类或单个测试方法
@Timeout(seconds=100)
这将设置测试方法运行所允许的最长时间。如果测试方法未能在规定的时间内完成,将发生两件事:
- 转储将打印到所有运行线程的堆栈外跟踪的System
- 它将在运行测试方法的线程上调用Thread.interrupt((,这将倾向于移动大多数"中断";等待I/O";以及";等待同步";案例
扩展类:
public class TimeoutExtension implements InvocationInterceptor {
private static final ScheduledExecutorService exec = Executors.newScheduledThreadPool(1);
@Override
public void interceptTestMethod(Invocation<Void> invocation, ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) throws Throwable {
Timeout timeout = invocationContext.getExecutable().getAnnotation(Timeout.class);
Class<?> clazz = invocationContext.getExecutable().getDeclaringClass();
while (timeout == null && clazz != Object.class) {
timeout = clazz.getAnnotation(Timeout.class);
clazz = clazz.getSuperclass();
}
if (timeout == null || timeout.seconds() <= 0) {
invocation.proceed();
return;
}
int seconds = timeout.seconds();
Thread caller = Thread.currentThread();
AtomicBoolean timedOut = new AtomicBoolean();
Future<Void> future = exec.schedule(() -> {
System.out.println("**** TIMEOUT ERROR: TEST EXCEEDED " + seconds + " SECONDS ****");
printThreadDump();
timedOut.set(true);
caller.interrupt();
return null;
}, seconds, TimeUnit.SECONDS);
Exception caught = null;
try {
invocation.proceed();
} catch (Exception ex) {
caught = ex;
} finally {
future.cancel(true);
if (timedOut.get()) {
if (timeout.expectTimeout()) {
// awesome!
} else {
Exception ex = new TimeoutException("Test exceeded timeout of " + seconds + " seconds");
if (caught != null) {
ex.addSuppressed(caught);
}
throw ex;
}
} else if (caught != null) {
throw caught;
} else if (timeout.expectTimeout()) {
throw new RuntimeException("Test expected to timeout at " + seconds + " but didn't");
}
}
}
}
超时注释
@Retention(RetentionPolicy.RUNTIME)
@Target( { ElementType.METHOD, ElementType.TYPE })
public @interface Timeout {
/** Set to zero to disable timeout **/
int seconds();
/** Set to true if the test is expected to timeout and this is OK with you.
This is mostly for self-testing the extension.
**/
boolean expectTimeout() default false;
}
最后,线程转储程序实用程序
public class TestUtils {
public static String generateThreadDump() {
final StringBuilder dump = new StringBuilder();
final ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
final ThreadInfo[] threadInfos = threadMXBean.getThreadInfo(threadMXBean.getAllThreadIds(), 100);
for (ThreadInfo threadInfo : threadInfos) {
dump.append('"');
dump.append(threadInfo.getThreadName());
dump.append("" ");
final Thread.State state = threadInfo.getThreadState();
dump.append("n java.lang.Thread.State: ");
dump.append(state);
final StackTraceElement[] stackTraceElements = threadInfo.getStackTrace();
for (final StackTraceElement stackTraceElement : stackTraceElements) {
dump.append("n at ");
dump.append(stackTraceElement);
}
dump.append("nn");
}
return dump.toString();
}
public static void printThreadDump() {
System.out.println(generateThreadDump());
}
}