如何在junit超时时转储所有线程堆栈——进行junit 5扩展



我正在使用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());
}
}

最新更新