bigywater.tech
Java · 375

JUnit 生命周期隔离

测试是否互相影响,往往取决于生命周期、顺序和并行这几个开关。这篇用最小的测试类把每个开关跑出来,看清隔离到底隔离了什么、没隔离什么。

每次一个新实例
JUnit Jupiter 默认(PER_METHOD)对每个测试方法与每次参数化 / 重复调用都新建测试类实例
319695 / 600000
并行 concurrent 下 3 个测试各 20 万次非原子 ++ 之后静态计数的值,3 个测试仍然全部通过
Random + PER_CLASS
6 个随机种子下共享 cart 的 3 个测试,每个种子都至少有 1 个失败(PER_METHOD 全部通过)

默认 PER_METHOD

实测 · 本机执行输出
  [LifeDefault]
      @BeforeAll(static)
      构造 #150
      @BeforeEach #150
      t1 #150 counter=1
      @AfterEach #150
      构造 #6f1
      @BeforeEach #6f1
      t2 #6f1 counter=1
      @AfterEach #6f1
      构造 #104
      @BeforeEach #104
      t3 #104 counter=1
      @AfterEach #104
      @AfterAll(static)
      → 0 tests aborted | 3 tests successful | 0 tests failed

PER_CLASS

实测 · 本机执行输出
  [LifePerClass]
      构造 #54c
      @BeforeAll(实例方法) #54c
      @BeforeEach #54c
      t1 #54c counter=1
      @AfterEach #54c
      @BeforeEach #54c
      t2 #54c counter=2
      @AfterEach #54c
      @BeforeEach #54c
      t3 #54c counter=3
      @AfterEach #54c
      @AfterAll(实例方法) #54c
      → 0 tests aborted | 3 tests successful | 0 tests failed

共享状态与随机顺序

实测 · 本机执行输出
  [PER_CLASS(一个实例共享 cart)]
      种子 1:通过 1 失败 2
      种子 2:通过 2 失败 1
      种子 3:通过 1 失败 2
      种子 4:通过 1 失败 2
      种子 5:通过 1 失败 2
      种子 6:通过 2 失败 1
  [默认 PER_METHOD(每个测试新实例)]
      种子 1:通过 3 失败 0
      种子 2:通过 3 失败 0
      种子 3:通过 3 失败 0
      种子 4:通过 3 失败 0
      种子 5:通过 3 失败 0
      种子 6:通过 3 失败 0

一个失败的执行轨迹

实测 · 本机执行输出
  [Stateful](配置 junit.jupiter.execution.order.random.seed=3)
      addApple 之后 cart=[apple]
      addPear 之后 cart=[apple, pear]
      emptyAtStart 看到 cart=[apple, pear]
      → 0 tests aborted | 1 tests successful | 2 tests failed

@Nested

实测 · 本机执行输出
  [Outer]
      Outer 构造 #330
      Outer @BeforeEach #330
      outerTest #330 outerCounter=1
      Outer 构造 #533
        Inner 构造 #304(外部实例 #533)
      Outer @BeforeEach #533
        Inner @BeforeEach #304
        i1 #304 innerCounter=1 outerCounter=1
      Outer 构造 #596
        Inner 构造 #7a5(外部实例 #596)
      Outer @BeforeEach #596
        Inner @BeforeEach #7a5
        i2 #7a5 innerCounter=1 outerCounter=1
      → 0 tests aborted | 3 tests successful | 0 tests failed

参数化与重复测试

实测 · 本机执行输出
  [Params]
      Params 构造 #4d1
      参数化 x=1 #4d1 n=1
      Params 构造 #500
      参数化 x=2 #500 n=1
      Params 构造 #e3b
      参数化 x=3 #e3b n=1
      Params 构造 #687
      重复 1 #687 n=1
      Params 构造 #2ee
      重复 2 #2ee n=1
      → 0 tests aborted | 5 tests successful | 0 tests failed

并行执行与共享静态状态

实测 · 本机执行输出
  [串行(默认)]
      a 完成(main)
      b 完成(main)
      c 完成(main)
      共享计数 shared = 600000(期望 600000,丢失 0)
      → 0 tests aborted | 3 tests successful | 0 tests failed
  [并行 concurrent](配置 junit.jupiter.execution.parallel.enabled=true junit.jupiter.execution.parallel.mode.default=concurrent)
      a 完成(ForkJoinPool-N-worker-N)
      b 完成(ForkJoinPool-N-worker-N)
      c 完成(ForkJoinPool-N-worker-N)
      共享计数 shared = 319695(期望 600000,丢失 280305)
      → 0 tests aborted | 3 tests successful | 0 tests failed
  [并行 + @ResourceLock](配置 junit.jupiter.execution.parallel.enabled=true junit.jupiter.execution.parallel.mode.default=concurrent)
      @ResourceLock 之后 shared = 600000(期望 600000)
      → 0 tests aborted | 3 tests successful | 0 tests failed
  [并行 + @Execution(SAME_THREAD)](配置 junit.jupiter.execution.parallel.enabled=true junit.jupiter.execution.parallel.mode.default=concurrent)
      @Execution(SAME_THREAD) 之后 shared = 600000(期望 600000)
      → 0 tests aborted | 3 tests successful | 0 tests failed

测试类之间的静态状态

实测 · 本机执行输出
  [先 Alpha 后 Beta]
      AlphaTest:Config.mode 原值 default,改成 alpha
      BetaTest 看到 Config.mode = alpha,loads = 1
      → 0 tests aborted | 2 tests successful | 0 tests failed
  [先 Beta 后 Alpha]
      BetaTest 看到 Config.mode = default,loads = 0
      AlphaTest:Config.mode 原值 default,改成 alpha
      → 0 tests aborted | 2 tests successful | 0 tests failed

@TempDir

实测 · 本机执行输出
  [Temp]
      one:perTest 目录 junit-N 内文件 1;shared 内文件 1
      two:perTest 内文件 1;shared 内文件 2(shared 里有上一个测试留下的文件)
      three:perTest 内文件 0;shared 内文件 2
      → 0 tests aborted | 3 tests successful | 0 tests failed

禁用与失败时的回调

实测 · 本机执行输出
  [DisabledCls]
      → 0 tests aborted | 0 tests successful | 0 tests failed
  [DisabledMethod]
      DisabledMethod 构造 #235
      DisabledMethod @BeforeEach
      on 运行
      DisabledMethod 构造 #846
      → 0 tests aborted | 1 tests successful | 0 tests failed
  [CtorFail]
      CtorFail @BeforeAll
      CtorFail 构造
      CtorFail 构造
      CtorFail @AfterAll
      → 0 tests aborted | 0 tests successful | 2 tests failed
  [BeforeEachFail]
      @BeforeEach 抛异常
      @AfterEach 仍然执行
      @AfterAll
      → 0 tests aborted | 0 tests successful | 1 tests failed

分步演示

JUnit 生命周期隔离
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64,JDK 21.0.12;JUnit Platform Console Standalone 1.11.3(JUnit Jupiter 5.11.3),通过 ConsoleLauncher 的 --select-class 与 --config 运行;事件日志由测试自己打印
    • 没有验证:Maven Surefire / Gradle 的类级并行与 forkCount 对隔离的影响、JUnit 4 的行为(每个测试方法新实例)、TestNG 的实例模型、jupiter 扩展(Extension)里的 BeforeEach 回调顺序、@Isolated 与 junit.jupiter.execution.parallel.config.* 的线程数配置、Spring 测试上下文缓存对静态与单例状态的跨类共享
    京ICP备2026067502号-1京公网安备11010502063487号