bigywater.tech
Java · 425

JUnit Nested Test:按场景组织测试层次

@Nested 让测试按“场景”组织成层次,但它的实例与生命周期、发现与选择、继承规则都有不直观的地方。这篇用真实的运行日志把它们逐项量出来。

10 次
三层 @Nested、5 个测试一共执行的 @BeforeEach 次数(构造函数调用也是 10 次)
0 个测试
--select-class Shop$WithCart$WithItem 直接选择嵌套类时找到的测试数
2 个 vs 3 个
--select-class WithStatic 与 --scan-class-path 找到的测试数(差别是没有 @Nested 的静态嵌套类)

三层嵌套的生命周期

实测 · 本机执行输出
  [Shop 整个类]
      构造 Shop #1
        @BeforeEach Shop(外层)#1
          测试 shopTest(外层)#1
        @AfterEach  Shop(外层)#1
      构造 Shop #2
        @BeforeEach Shop(外层)#2
          测试 EmptyCart.total
        @AfterEach  Shop(外层)#2
      构造 Shop #3
      构造 WithCart #4(外层 Shop #3)
        @BeforeEach Shop(外层)#3
        @BeforeEach WithCart(第 2 层)#4
          测试 cartTest(第 2 层)#4
        @AfterEach  WithCart(第 2 层)#4
        @AfterEach  Shop(外层)#3
      构造 Shop #5
      构造 WithCart #6(外层 Shop #5)
      构造 WithItem #7(外层 WithCart #6,最外层 Shop #5)
        @BeforeEach Shop(外层)#5
        @BeforeEach WithCart(第 2 层)#6
        @BeforeEach WithItem(第 3 层)#7
          测试 itemTest1(第 3 层)#7
        @AfterEach  WithItem(第 3 层)#7
        @AfterEach  WithCart(第 2 层)#6
        @AfterEach  Shop(外层)#5
      构造 Shop #8
      构造 WithCart #9(外层 Shop #8)
      构造 WithItem #10(外层 WithCart #9,最外层 Shop #8)
        @BeforeEach Shop(外层)#8
        @BeforeEach WithCart(第 2 层)#9
        @BeforeEach WithItem(第 3 层)#10
          测试 itemTest2(第 3 层)#10
        @AfterEach  WithItem(第 3 层)#10
        @AfterEach  WithCart(第 2 层)#9
        @AfterEach  Shop(外层)#8
      → 5 tests found | 0 tests skipped | 5 tests successful | 0 tests failed
  汇总:共 5 个测试方法被执行;@BeforeEach 总共执行 10 次;构造函数总共被调用 10 次;外层 Shop 被构造 5 次

树形输出与选择

实测 · 本机执行输出
  [tree 输出]
      构造 Shop #1
        @BeforeEach Shop(外层)#1
          测试 shopTest(外层)#1
        @AfterEach  Shop(外层)#1
      构造 Shop #2
        @BeforeEach Shop(外层)#2
          测试 EmptyCart.total
        @AfterEach  Shop(外层)#2
      构造 Shop #3
      构造 WithCart #4(外层 Shop #3)
        @BeforeEach Shop(外层)#3
        @BeforeEach WithCart(第 2 层)#4
          测试 cartTest(第 2 层)#4
        @AfterEach  WithCart(第 2 层)#4
        @AfterEach  Shop(外层)#3
      构造 Shop #5
      构造 WithCart #6(外层 Shop #5)
      构造 WithItem #7(外层 WithCart #6,最外层 Shop #5)
        @BeforeEach Shop(外层)#5
        @BeforeEach WithCart(第 2 层)#6
        @BeforeEach WithItem(第 3 层)#7
          测试 itemTest1(第 3 层)#7
        @AfterEach  WithItem(第 3 层)#7
        @AfterEach  WithCart(第 2 层)#6
        @AfterEach  Shop(外层)#5
      构造 Shop #8
      构造 WithCart #9(外层 Shop #8)
      构造 WithItem #10(外层 WithCart #9,最外层 Shop #8)
        @BeforeEach Shop(外层)#8
        @BeforeEach WithCart(第 2 层)#9
        @BeforeEach WithItem(第 3 层)#10
          测试 itemTest2(第 3 层)#10
        @AfterEach  WithItem(第 3 层)#10
        @AfterEach  WithCart(第 2 层)#9
        @AfterEach  Shop(外层)#8
      → 5 tests found | 0 tests skipped | 5 tests successful | 0 tests failed
        ├─ JUnit Platform Suite ✔
        ├─ JUnit Jupiter ✔
        │  └─ Shop ✔
        │     ├─ shopTest() ✔
        │     ├─ 空购物车 ✔
        │     │  └─ 总价为 0 ✔
        │     └─ WithCart ✔
        │        ├─ cartTest() ✔
        │        └─ WithItem ✔
        │           ├─ itemTest1() ✔
        │           └─ itemTest2() ✔
        └─ JUnit Vintage ✔
  想只运行嵌套类里的一部分,控制台启动器的几种选择方式的结果:
  [--select-class Shop$WithCart$WithItem(直接选择最内层类)]
      → 0 tests found | 0 tests skipped | 0 tests successful | 0 tests failed
  [--select-method Shop$WithCart#cartTest]
      → 0 tests found | 0 tests skipped | 0 tests successful | 0 tests failed
  [--select-class Shop --include-classname '.*WithItem']
      构造 Shop #1
        @BeforeEach Shop(外层)#1
          测试 shopTest(外层)#1
        @AfterEach  Shop(外层)#1
      构造 Shop #2
        @BeforeEach Shop(外层)#2
          测试 EmptyCart.total
        @AfterEach  Shop(外层)#2
      构造 Shop #3
      构造 WithCart #4(外层 Shop #3)
        @BeforeEach Shop(外层)#3
        @BeforeEach WithCart(第 2 层)#4
          测试 cartTest(第 2 层)#4
        @AfterEach  WithCart(第 2 层)#4
        @AfterEach  Shop(外层)#3
      构造 Shop #5
      构造 WithCart #6(外层 Shop #5)
      构造 WithItem #7(外层 WithCart #6,最外层 Shop #5)
        @BeforeEach Shop(外层)#5
        @BeforeEach WithCart(第 2 层)#6
        @BeforeEach WithItem(第 3 层)#7
          测试 itemTest1(第 3 层)#7
        @AfterEach  WithItem(第 3 层)#7
        @AfterEach  WithCart(第 2 层)#6
        @AfterEach  Shop(外层)#5
      构造 Shop #8
      构造 WithCart #9(外层 Shop #8)
      构造 WithItem #10(外层 WithCart #9,最外层 Shop #8)
        @BeforeEach Shop(外层)#8
        @BeforeEach WithCart(第 2 层)#9
        @BeforeEach WithItem(第 3 层)#10
          测试 itemTest2(第 3 层)#10
        @AfterEach  WithItem(第 3 层)#10
        @AfterEach  WithCart(第 2 层)#9
        @AfterEach  Shop(外层)#8
      → 5 tests found | 0 tests skipped | 5 tests successful | 0 tests failed
  [--select-class Shop --include-tag nonexistent(对照:标签过滤)]
      → 0 tests found | 0 tests skipped | 0 tests successful | 0 tests failed

静态嵌套类

实测 · 本机执行输出
  [--select-class WithStatic]
          测试 WithStatic.outerTest
          测试 WithStatic.IsNested.nestedTest
      → 2 tests found | 0 tests skipped | 2 tests successful | 0 tests failed
  [--scan-class-path 且匹配 WithStatic.*]
          测试 WithStatic.NotNested.staticNestedTest(没有 @Nested 的静态嵌套类)
          测试 WithStatic.outerTest
          测试 WithStatic.IsNested.nestedTest
      → 3 tests found | 0 tests skipped | 3 tests successful | 0 tests failed

嵌套类里的 @BeforeAll

实测 · 本机执行输出
  [BeforeAllNested]
      @BeforeAll(外层,static)
          测试 outerTest
      @BeforeAll(PER_CLASS 嵌套类里的实例方法)
          测试 PerClassAll.t1 counter=1
          测试 PerClassAll.t2 counter=2
      @BeforeAll(嵌套类里的 static 方法)
          测试 StaticAll.t1
          测试 StaticAll.t2
      @AfterAll(外层,static)
      → 5 tests found | 0 tests skipped | 5 tests successful | 0 tests failed

扩展 / 标签 / @Disabled 的继承

实测 · 本机执行输出
  [ExtOuter 全部]
          测试 ExtOuter.plain
          测试 NoExt.c(没有扩展)
        扩展 beforeEach:WithExt.a()(第 1 次)
          测试 WithExt.a
        扩展 beforeEach:Deeper.b()(第 2 次)
          测试 WithExt.Deeper.b(继承了外层的 @ExtendWith 与 @Tag)
      → 5 tests found | 1 tests skipped | 4 tests successful | 0 tests failed
  [ExtOuter,--include-tag slow]
        扩展 beforeEach:WithExt.a()(第 1 次)
          测试 WithExt.a
        扩展 beforeEach:Deeper.b()(第 2 次)
          测试 WithExt.Deeper.b(继承了外层的 @ExtendWith 与 @Tag)
      → 2 tests found | 0 tests skipped | 2 tests successful | 0 tests failed
  [DisabledOuter(外层 @Disabled,里面有 @Nested)]
      → 2 tests found | 2 tests skipped | 0 tests successful | 0 tests failed

分步演示

JUnit @Nested
本步的对象

    本文用到的工具与边界

    • 本机 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(Jupiter 5.11.3);每个场景都是新的 ConsoleLauncher 进程;日志由测试里的 H.log 打印
    • 没有验证:Maven Surefire / Gradle 的类选择行为(IDE 里可以直接运行单个 @Nested 类,可能走的是 NestedClassSelector 的路径)、@Nested 与参数化测试 / @TestFactory 的组合、@TempDir 在嵌套类里的生命周期、并行执行下的嵌套类、JUnit 6 的变化
    京ICP备2026067502号-1京公网安备11010502063487号