bigywater.tech
Java · 380

JUnit 超时测试:防止阻塞悄悄拖慢构建

超时能挡住卡死的测试吗?这篇用 JUnit 5.11 把四种写法都跑了一遍:谁真的缩短了耗时,谁只是事后报告,放弃的线程会留下什么,以及超时设得太紧时机器一忙会怎样。

3.3 s
@Timeout(1) 的忙等测试(不检查中断)整个 JVM 的用时,超时只是把它判为失败
null
@Timeout 的 SEPARATE_THREAD 模式下读到的 ThreadLocal 值(SAME_THREAD 读到的是设置的值)
292–296 / 300
60 ms 超时在 40 个空转进程的满载机器上失败的次数(空闲机器 0 次)

默认的中断超时

实测 · 本机执行输出
  [SleepTO] 整个 JVM 用时 1.3 s
      [    0 ms] sleep 开始
      [ 1010 ms] sleep 被中断
      │ └─ sleeps3s() ✘ sleeps3s() timed out after 1 second
      => java.util.concurrent.TimeoutException: sleeps3s() timed out after 1 second
      → 0 tests successful | 1 tests failed
  [BusyTO] 整个 JVM 用时 3.3 s
      [    0 ms] 忙等 3 秒开始(不检查中断)
      [ 3005 ms] 忙等结束,方法返回
      │ └─ busy3s() ✘ busy3s() timed out after 1 second
      => java.util.concurrent.TimeoutException: busy3s() timed out after 1 second
      → 0 tests successful | 1 tests failed

SEPARATE_THREAD

实测 · 本机执行输出
  [SepTO] 整个 JVM 用时 1.3 s
      [    0 ms] 忙等 3 秒开始,线程 非 main 线程
      │ └─ busy3s() ✘ busy3s() timed out after 1 second
      => java.util.concurrent.TimeoutException: busy3s() timed out after 1 second
      → 0 tests successful | 1 tests failed
  [SepTO + AfterTO 在同一个 JVM 里] 整个 JVM 用时 4.8 s
      [    0 ms] 忙等 3 秒开始,线程 非 main 线程
      [ 1017 ms] 另一个测试开始(sleep 3.5 秒,模拟后面的测试)
      [ 3006 ms] 忙等结束(测试早已被判失败)
      [ 4528 ms] 另一个测试结束
      │ │ └─ busy3s() ✘ busy3s() timed out after 1 second
      => java.util.concurrent.TimeoutException: busy3s() timed out after 1 second
      → 1 tests successful | 1 tests failed
  [TlTO] 整个 JVM 用时 0.3 s
      [    0 ms] @BeforeEach 在 main 里设置 ThreadLocal
      [    7 ms] SAME_THREAD(默认):在 main 里读到 ThreadLocal = 事务上下文
      [   11 ms] @BeforeEach 在 main 里设置 ThreadLocal
      [   12 ms] SEPARATE_THREAD:在 另一个线程 里读到 ThreadLocal = null
      → 2 tests successful | 0 tests failed

assertTimeout

实测 · 本机执行输出
  [AssertTO] 整个 JVM 用时 4.3 s
      [    0 ms] assertTimeoutPreemptively 开始(sleep 3 秒)
      [ 1018 ms] lambda 被中断
      [ 1030 ms] assertTimeout 开始(sleep 3 秒)
      [ 4041 ms] assertTimeout 的 lambda 跑完了
      │ ├─ preemptive() ✘ execution timed out after 1000 ms
      │ └─ nonPreemptive() ✘ execution exceeded timeout of 1000 ms by 2010 ms
      => org.opentest4j.AssertionFailedError: execution timed out after 1000 ms
      => org.opentest4j.AssertionFailedError: execution exceeded timeout of 1000 ms by 2010 ms
      → 0 tests successful | 2 tests failed

全局默认超时

实测 · 本机执行输出
  [NoAnno 不设默认超时] 整个 JVM 用时 3.3 s
      [    0 ms] @BeforeEach 卡住 3 秒(没有任何 @Timeout)
      [ 3015 ms] @BeforeEach 结束
      [ 3021 ms] test 运行
      → 1 tests successful | 0 tests failed
  [NoAnno 默认超时 1s](配置 junit.jupiter.execution.timeout.default=1s) 整个 JVM 用时 1.3 s
      [    0 ms] @BeforeEach 卡住 3 秒(没有任何 @Timeout)
      │ └─ t() ✘ hangs() timed out after 1 second
      => java.util.concurrent.TimeoutException: hangs() timed out after 1 second
      → 0 tests successful | 1 tests failed

超时与机器负载

实测 · 本机执行输出
  逻辑 CPU 10 个
  空闲机器 第 1 次:成功 300、失败 0
  空闲机器 第 2 次:成功 300、失败 0
  空闲机器 第 3 次:成功 300、失败 0
  满载机器(40 个空转进程)第 1 次:成功 8、失败 292
  满载机器(40 个空转进程)第 2 次:成功 8、失败 292
  满载机器(40 个空转进程)第 3 次:成功 4、失败 296

分步演示

JUnit 超时
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12;JUnit console-standalone 1.11.3;测试机 10 个逻辑 CPU;满载用 40 个 python 空转进程制造
    • 没有验证:Maven Surefire / Gradle 的 forkedProcessTimeout 与 test timeout、JUnit 4 的 Timeout rule、并行执行下的超时、JVM 在最后一个非守护线程仍在运行时的退出行为(这里只观察到 ConsoleLauncher 退出时结束了被放弃的线程)、阻塞在不可中断 I/O 上的线程
    京ICP备2026067502号-1京公网安备11010502063487号