OpenJDK HotSpot 原生单元测试开发指南:基于 GoogleTest 的 TEST / TEST_VM / TEST_OTHER_VM 实践全解
【免费下载链接】jdkJDK main-line development https://openjdk.org/projects/jdk项目地址: https://gitcode.com/GitHub_Trending/jd/jdk
本文以 OpenJDK 主线仓库(本仓库)中的 doc/hotspot-unit-tests.md 为骨架,结合 test/hotspot/gtest/ 下真实的测试源码与 make/hotspot/test/GtestImage.gmk 等构建设施,系统讲解在 HotSpot 中使用 GoogleTest 编写原生(C++)单元测试时应遵循的设计原则、断言规范、命名约定与各类限制。读完本文,你将掌握如何判断一个测试应该使用
TEST、TEST_VM还是TEST_OTHER_VM,如何写出具备隔离性、可重复性、信息量的测试,以及如何正确恢复 JVM 标志位、处理_JAVA_OPTIONS与@requires等实战细节。
一、这份指南解决什么问题
HotSpot 是 OpenJDK 的运行时核心(JIT 编译器、GC、类加载、线程管理等全部原生实现所在),其内部大量组件是无法通过纯 Java 测试触及的。为此,OpenJDK 引入了 GoogleTest(C++ 单元测试框架)来编写 HotSpot 的原生测试,测试源码统一存放在 test/hotspot/gtest/ 目录。
doc/hotspot-unit-tests.md 的定位不是"如何安装 GoogleTest",而是为 HotSpot 团队建立一套共享的测试开发愿景:既包括对所有语言、所有框架普遍成立的"好测试属性",也包括 HotSpot 与 GoogleTest 集成后特有的约束(例如TEST_OTHER_VM内部不能使用 death test、外部标志不能直接传给被测 JVM 等)。新提交的测试代码都应当遵循这份指南,否则在 review 阶段会被要求修改。
二、好测试的七项核心属性
指南第一小节定义了对几乎所有测试类型(无论语言与框架)都成立的"好测试属性",这是后续所有具体建议的理论根基。
2.1 Lightness(轻量性):选用最轻量的测试类型
在 HotSpot 中,根据对 JVM 的依赖程度,测试分为三个层级,每一级都比上一级更慢:
| 测试类型 | 对 JVM 的依赖 | 特征 |
|---|---|---|
TEST | 完全不依赖 JVM | 最轻量,纯逻辑/数据结构测试 |
TEST_VM | 依赖一个已初始化的 JVM | 正常运行,但不允许破坏 JVM(须保持 JVM 处于可工作状态) |
TEST_OTHER_VM | 依赖 JVM,且需要全新初始化的 JVM | 允许把 JVM 弄到不可工作的状态 |
之所以要区分这三个层级,是因为同一测试进程内所有TEST_VM测试共享同一个已初始化的 JVM——如果你的测试会"彻底改变 JVM 状态且不恢复",就应该考虑使用TEST_OTHER_VM(详见 test/hotspot/gtest/unittest.hpp 中三个宏的定义)。
2.2 Isolation(隔离性):测试之间互不影响
一个测试不能产生可见的副作用,不能影响其他测试的结果。测试结果不应依赖执行顺序或其他测试,否则当测试失败时将几乎无法定位根因。由于 HotSpot 的特殊性,完全隔离并不容易——例如TEST_VM测试共享同一个已初始化的 JVM,因此如果某个测试过度改变了 JVM 状态且不恢复,就应改用TEST_OTHER_VM。
2.3 Atomicity and self-containment(原子性与自包含)
一个测试应原子且自包含:
- 一个测试只检查某个类、子系统或功能的特定一部分,这样当测试失败时,很容易判断产品中哪部分坏了;
- 同时,该测试应当"比较完整地"覆盖这一部分——当别人看到
FooTest::bar时,会默认Foo中bar的所有方面都被测到了。
如果某方法有多个行为面向(例如参数为null时、参数合法但对象状态不允许时……),应当一个面向写一个测试。这不仅能保证原子性和自包含,也让测试名具有自描述性(见下文"测试命名")。
2.4 Repeatability(可重复性)
测试必须可重复。偶发(sporadic)失败最难调查、最难修复、也最难验证修复是否有效。有些场景很难做到 100% 可重复——例如TEST_VM中有多个并发线程在跑——但我们仍应尽可能让测试可复现。
2.5 Informativeness(信息量):失败时尽量多给线索
测试失败时,提供的信息越多越好。除了被比较的值,额外的上下文可以大幅缩短甚至消灭调试时间;对不可 100% 复现的失败尤其重要。
但注意,信息量属性很容易走向反面——测试过于啰嗦,反而让人在信息海洋里找不到有用信息。因此要同时考虑"提供什么样的信息"(见下文"错误消息")与"什么时候才输出"(见下文"无干扰输出")。
2.6 Testing instead of visiting(测试而不是"路过")
测试必须真的在测试。"访问"(visit)某段代码是不够的,测试应当:
- 检查代码是否完成了它该做的事;
- 把返回值与期望值比较;
- 检查期望的副作用发生了、不期望的副作用没发生,等等。
换言之,一个测试至少应包含一条 GoogleTest 断言,且不能依赖 JVM 自身的 assert。写一个好测试的一般方法是:先建立被测系统的模型、可能 bug 的模型(或你想发现的 bug 的模型),再基于这些模型设计测试。
2.7 Nearness(就近性):检查逻辑尽量放在测试内
优先把检查放在测试代码内部。如果把测试逻辑(例如验证方法)外置、依赖产品代码中的 assert,会违背前面多条原则,还降低可读性和稳定性。当所有测试逻辑都位于测试内或共享测试库中时,理解"这个测试在测什么"要容易得多。经验法则:检查离测试越近越好。
三、断言(Asserts)规范
3.1 多条检查时优先EXPECT,而非ASSERT
EXPECT(非致命失败)与ASSERT(致命失败,立即中止当前函数)的选择与"信息量"属性直接相关:失败后继续执行剩余检查,能提供更多信息以帮助定位缺陷根因。只有当无法继续执行测试或继续执行没有意义时,才使用ASSERT。下文统一用EXPECT代指ASSERT/EXPECT两种形式。
当有多个相互独立的检查、但任何一个失败都导致后续无法继续时,可以使用::testing::Test::HasNonfatalFailure(),推荐写法是:
ASSERT_FALSE(::testing::Test::HasNonfatalFailure());这既明确说明了测试为何中止,也允许你在失败时附带更多信息。
3.2 第一个参数永远是期望值
所有相等性断言中,期望值必须作为第一个参数。这也是 GoogleTest 的惯例,且 GoogleTest 对两个参数的处理有细微差别——最典型的是null检测:null检测只对第一个参数生效,即
EXPECT_EQ(NULL, object); // 检查 object 是否为 null EXPECT_EQ(object, NULL); // 检查 object 是否等于 NULLGoogleTest 对比较值的类型要求非常严格,因此EXPECT_EQ(object, NULL)这种写法通常会直接产生编译期错误。
3.3 浮点数比较:使用专用宏
由于浮点数表示与舍入误差,常规相等比较在大多数情况下不会返回true。GoogleTest 提供了:
EXPECT_FLOAT_EQ/EXPECT_DOUBLE_EQ:检查两个值的距离不超过4 ULP(Unit in the Last Place);EXPECT_NEAR(v1, v2, eps):检查v1与v2之差的绝对值不超过eps。
3.4 C 字符串比较:使用字符串专用宏
EXPECT_EQ对 C 字符串只会比较指针值,这通常不是你要的。GoogleTest 提供:
EXPECT_STREQ/EXPECT_STRNE:比较 C 字符串内容;- 大小写不敏感版本:
EXPECT_STRCASEEQ/EXPECT_STRCASENE。
3.5 错误消息:信息充分但不冗余
所有 GoogleTest 断言都会自动打印被比较的表达式及其值,因此错误消息里不需要重复这些内容。但注意断言只打印被比较的值,不打印任何中间变量——例如ASSERT_TRUE((val1 == val2 && isFail(foo(8))) || i == 18)只会打印一个值。
- 如果使用了复杂谓词,请考虑
EXPECT_PRED*或EXPECT_FORMAT_PRED断言族:它们检查谓词返回真/成功,并打印所有参数的值; - 默认信息不够时(典型场景:循环内的断言,GoogleTest 不会打印迭代次数),可以用
<<运算符向断言追加信息,例如打印错误码与对应错误消息、打印可能影响结果内部状态等。
3.6 无干扰输出:仅在需要时打印
测试通过时也打印所有信息的做法是很差的实践,它会污染输出,让有用信息更难被找到。指南给出的推荐做法是:把信息先保存到临时缓冲区,再传给断言。
仓库中 test/hotspot/gtest/gc/shared/test_memset_with_concurrent_readers.cpp 就是教科书式的例子:TEST(gc, memset_with_concurrent_readers)用三层循环穷举memset_with_concurrent_readers的各种起始/结束位置组合,只有在head_clear && middle_set && tail_clear不成立时才构造stringStream并逐 chunk、逐行打印 8 字节十六进制内容,最后通过ASSERT_TRUE(...) << err_stream.freeze()把缓冲内容作为断言消息输出:
if (!(head_clear && middle_set && tail_clear)) { stringStream err_stream{}; err_stream.print_cr("*** memset_with_concurrent_readers failed: " "set start %zu, set end %zu", set_start, set_end); for (unsigned chunk = 0; chunk < (block_size / chunk_size); ++chunk) { for (unsigned line = 0; line < (chunk_size / BytesPerWord); ++line) { const char* lp = &block[chunk * chunk_size + line * BytesPerWord]; err_stream.print_cr("%u, %u: %02x %02x %02x %02x %02x %02x %02x %02x", chunk, line, line_byte(lp, 0), line_byte(lp, 1), line_byte(lp, 2), line_byte(lp, 3), line_byte(lp, 4), line_byte(lp, 5), line_byte(lp, 6), line_byte(lp, 7)); } } EXPECT_TRUE(head_clear) << "leading byte not clear"; EXPECT_TRUE(middle_set) << "memset byte not set"; EXPECT_TRUE(tail_clear) << "trailing bye not clear"; ASSERT_TRUE(head_clear && middle_set && tail_clear) << err_stream.freeze(); }这个示例同时示范了文档中的"无干扰输出""信息量""多检查优先 EXPECT"三条原则的组合运用。
3.7 失败传播:用(EXPECT|ASSERT)_NO_FATAL_FAILURE包裹子例程
ASSERT和FAIL只会中止当前函数。如果它们出现在某个子例程中,即使子例程内断言失败,测试主体也不会被中止。因此应当用ASSERT_NO_FATAL_FAILURE包裹这类子例程调用,从而传播致命失败并中止测试;(EXPECT|ASSERT)_NO_FATAL_FAILURE也可用于补充更多信息。
由于显而易见的理由,不存在(EXPECT|ASSERT)_NO_NONFATAL_FAILURE宏。如果确实需要检查子例程是否产生了非致命失败(某个EXPECT失败),可以使用:
::testing::Test::HasNonfatalFailure():检查是否产生了非致命失败;::testing::Test::HasFailure():检查是否产生了任意失败。
四、命名与分组(Naming and Grouping)
命名规范的意义在于:方便查找测试、过滤测试、简化失败分析。一个测试名不好,其生命周期内的所有环节(规划、盘点、评审、失败分析、演进)都会受影响。
4.1 测试组名:CamelCase
- 测试组名使用CamelCase,以字母开头和结尾;
- 以被测类、功能、子系统命名。
例如:类Foo→ 测试组Foo;编译器日志子系统 →CompilerLogging;G1 GC →G1GC。
4.2 文件名:test_前缀 +.cpp后缀
测试文件必须以test_开头、以.cpp结尾。这不是风格偏好,而是当前构建系统识别测试文件的实际要求。打开 test/hotspot/gtest/ 即可验证:test_freeRegionList.cpp、test_g1Analytics.cpp、test_heapRegion.cpp等全部遵循该约定。
4.3 文件位置:镜像被测代码的目录结构
测试文件的位置应反映被测产品部分的位置:
- 针对
foo/bar/baz.cpp中某个类的单元测试,应放在 test/hotspot/gtest/ 下的foo/bar/test_baz.cpp。一个类的所有测试集中在同一文件是单元测试的常见实践,便于一览全部现有测试、在不破坏封装的前提下共享函数与资源; - 针对多个类的测试:目录层级应与产品层级一致,文件名反映被测子系统/功能的名称。例如被测子系统属于
gc/g1,测试就放在gc/g1目录——仓库中的 test/hotspot/gtest/gc/g1/ 正是这样组织的。
注意:框架会把目录名拼接到测试组名前。例如在test/hotspot/gtest/gc/shared/test_foo.cpp中定义的TEST(foo, check_this)与TEST(bar, check_that),最终会以gc/shared/foo::check_this和gc/shared/bar::check_that的形式被报告。这也解释了为什么组名和目录会同时出现在测试报告中。
4.4 测试名:小写蛇形(small_snake_case)
- 测试名使用小写蛇形(small_snake_case),以字母开头和结尾;
- 测试名应反映"这个测试在检查什么"。
示例对比(来自指南原文):
foo_return_0_if_name_is_null优于foo_sanity、foo_basic或foo;humongous_objects_can_not_be_moved_by_young_gc优于ho_young_gc。
指南还坦诚指出:使用下划线其实违反 GoogleTest 项目自身的约定(可能导致非法标识符),但该约束过于严格。只要仅为测试名使用下划线、并禁止测试名以下划线开头或结尾,就足够安全。
4.5 Fixture 类:类名 +Test后缀
Fixture 类应以被测类/子系统命名(遵循测试组命名规则),并加上Test后缀以避免类名冲突。例如 test/hotspot/gtest/gc/g1/test_g1IHOPControl.cpp 中的G1IHOPTestController、test/hotspot/gtest/aarch64/test_spin_pause.cpp 等文件中的 fixture,都遵循"被测对象名 + Test 语义"的模式。
4.6 Friend 类:Test或Testable后缀
所有用于测试目的的 friend 类都应带Test或Testable后缀。这能大幅简化对 friendship 用途的理解,并允许静态检查私有成员没有被意外暴露。例如FooTest作为Foo的 friend 而无任何注释,会被理解为"为了可测试性不得不做的必要之恶"。
4.7 OS/CPU 特定测试:#ifdef守卫 + 文件名带平台名
用#ifdef守卫 OS/CPU 特定的测试,并在文件名中包含 OS/CPU 名称。当前构建系统尚不支持 OS、CPU、OS-CPU 特定测试的独立目录;将来这类测试增多时,会像 HotSpot 主体那样调整目录布局与构建系统。仓库中 test/hotspot/gtest/aarch64/、test/hotspot/gtest/riscv/、test/hotspot/gtest/s390/、test/hotspot/gtest/x86/ 等目录即为按 CPU 架构组织测试的实例。
五、杂项规范(Miscellaneous)
5.1 Hotspot 风格
测试是 HotSpot 的一部分,因此 HotSpot 风格指南中适用的一切规范都同样适用于测试。本指南只覆盖测试特有的内容。(可参考仓库中的 doc/hotspot-style.md。)
5.2 代码/测试度量
覆盖率等信息对决定"该写什么测试、该改进什么测试、能删掉什么测试"非常有用。对单元测试而言,分支覆盖率(branch coverage)是广泛使用且公认的度量,它能在相对简单的测试开发流程下提供良好的测试质量。对其他层级的测试,分支覆盖率并不适用,应改用事务流覆盖(transaction flow coverage)、数据流覆盖(data flow coverage)等其他度量。
5.3 访问非公有成员:显式 friend 类
获取非公有成员访问权应使用显式 friend 类声明。HotSpot 不使用 GoogleTest 提供的 friendship 宏,因为显式声明更清晰。把测试 fixture 类声明为被测类的 friend 是最简单、最清晰的方式,但它有两个缺点:
- 每个测试都要被声明为 friend;
- 子类不会继承 friendship 关系。
换句话说,这会加大测试间共享代码的难度。因此若打算共享代码或预期代码对其他测试有用,应优先考虑:
- 把被测类的成员改为
protected,并引入一个共享的测试专用类,通过公有函数暴露这些成员; - 甚至直接在产品类中把成员设为公有可访问;
- 若无法修改成员可见性,则创建一个暴露成员的 friend 类。
5.4 Death tests:在TEST_OTHER_VM与TEST_VM_ASSERT*中禁止使用
不能在TEST_OTHER_VM和TEST_VM_ASSERT*内部使用 death test 功能。原因是实现层面:TEST_OTHER_VM与TEST_VM_ASSERT*本身就是以 GoogleTest death test 形式实现的,而 GoogleTest不允许 death test 嵌套在另一个 death test 内。查看 test/hotspot/gtest/unittest.hpp 中TEST_OTHER_VM的定义即可确认:它的外层主体就是一个ASSERT_EXIT(child_..., ::testing::ExitedWithCode(0), ".*OKIDOKI.*")。
5.5 外部标志:不支持向被测 JVM 传递外部标志
将外部标志传给被测 JVM目前不支持。这是刻意的设计决策:为了简化测试与测试框架本身,并避免不兼容标志组合导致的失败,直到出现好的解决方案为止。但如果确实需要以特定标志组合测试 JVM,可以使用_JAVA_OPTIONS环境变量——来自_JAVA_OPTIONS的标志会作用于TEST_VM、TEST_OTHER_VM和TEST_VM_ASSERT*测试。
5.6 测试专用标志:尚未实现,先用if (!<flag>) return+@requires注释
在TEST_OTHER_VM和TEST_VM_ASSERT*中传递测试专用标志的能力是需要的(系统测试、回归测试等需要以特定配置运行完整 JVM,例如选择 Serial GC),但尚未实现,计划在后续版本中加入。
目前的临时 workaround 是:如果测试依赖某个标志值,应在测试最开头加if (!<flag>) { return; }守卫,并在测试宏正上方加一段类似 jtreg@requires指令的注释。指南明确指出:必须遵循这个模式,因为这样便于日后统一找到这些测试,在标志传递设施实现后一次性更新。
仓库中的真实范例正是 test/hotspot/gtest/gc/g1/test_g1IHOPControl.cpp:
TEST_VM(G1IHOPControl, allocation_tracker_incr) { // Test requires G1 if (!UseG1GC) { return; } size_t initial_ihop = InitiatingHeapOccupancyPercent; G1IHOPTestController ctrl(false /* adaptive */, initial_ihop, 100 /* target_occupancy */); ... EXPECT_EQ(20u, ctrl.non_humongous_allocated_bytes()); EXPECT_EQ(30u, ctrl.peak_extra_humongous_occupancy_bytes()); ... }该文件(共 824 行)围绕G1IHOPControl的分配追踪、自适应/非自适应 IHOP 阈值、并发周期记录等行为组织了多个TEST_VM用例,并用G1IHOPTestController封装了对多个 G1 组件的调用序列,是"测试逻辑就近封装 + 标志守卫 + 自描述测试名"的综合范例。
长期规划中,期望 jtreg 把 GoogleTest 测试作为一等公民支持:解析@requires注释并自动过滤不适用测试。
5.7 标志恢复:改过的标志要改回来
测试经常通过改变标志值来配置 JVM。GoogleTest 提供两种"测试前设置环境、测试后恢复"的方式:构造/析构函数,或SetUp/TearDown函数——两者都需要 fixture 类,有时过于啰嗦。更简单的设施是FLAG_GUARD宏或*FlagSetting类,用于在局部作用域内恢复/设置值。
仓库中的真实用法:
- test/hotspot/gtest/gc/serial/test_collectorPolicy.cpp 用
AutoSaveRestore<size_t> FLAG_GUARD(MinHeapSize);等一次性保存并恢复MinHeapSize、InitialHeapSize、MaxHeapSize、MaxNewSize、MinHeapDeltaBytes、NewSize; - test/hotspot/gtest/aarch64/test_assembler_aarch64.cpp 与 test/hotspot/gtest/oops/test_markWord.cpp 使用
FlagSetting fs(AlwaysMergeDMB, true/false)、FlagSetting fs(WizardMode, true)在局部作用域内临时设置标志,作用域结束自动恢复。
注意事项:
- 改变标志值可能破坏标志之间的不变量,从而把 JVM 带到意外/不支持的状態;
FLAG_SET_*宏可能为了维持不变量而同时改变多个标志,很难预测到底改了哪些、也难以完整恢复。因此使用FLAG_SET_*宏的测试应当使用TEST_OTHER_VM测试类型(让 JVM 以全新状态启动,规避恢复问题)。
5.8 GoogleTest 文档
凡涉及 GoogleTest 本身的问题——断言、测试声明宏、其他宏等——请查阅 GoogleTest 的官方文档。
六、测试类型宏的源码级解读
文档在 TODO 部分列出了一些待补充的内容(测试类型的用途/缺点/限制、测试库、随机顺序运行、mocks/stubs、setUp/tearDown 等),但其中最关键的一环——各测试类型的宏定义——已经可以在仓库中直接读到。下面结合 test/hotspot/gtest/unittest.hpp 补充说明:
TEST(category, name)直接展开为GTEST_TEST(category, name),即普通 GoogleTest 测试,不依赖 JVM;TEST_VM(category, name)展开为GTEST_TEST(category, CONCAT(name, _vm))——注意测试名会被追加_vm后缀,并且这些测试运行在共享的已初始化 JVM 中;TEST_VM_F(test_fixture, name)对应带 fixture 的 JVM 测试,同样追加_vm后缀;TEST_OTHER_VM(category, name)定义了一个子函数test_<category>_<name>_(),外层TEST通过ASSERT_EXIT(child_..., ::testing::ExitedWithCode(0), ".*OKIDOKI.*")以 death test 方式启动子 JVM 执行,子进程退出前调用JNI_GetCreatedJavaVMs获取 VM 并DestroyJavaVM,然后打印OKIDOKI后退出——这就解释了为何该类型内部不能再嵌套 death test,以及为什么它能容忍 JVM 处于不可工作状态(反正每次都是全新 JVM);TEST_VM_ASSERT/TEST_VM_ASSERT_MSG(以及TEST_VM_FATAL_ERROR_MSG、TEST_VM_CRASH_SIGNAL)仅在debug 构建(ASSERT定义)下可用,同样通过ASSERT_EXIT子进程方式验证 JVM 断言失败路径,期望退出码为 1 并匹配"assert failed"等消息。
七、构建与运行路径
原生 gtest 测试随测试镜像一起构建:make/hotspot/test/GtestImage.gmk 展示了构建系统如何把每个 JVM variant 编译出的libjvm(gtest 版)与gtestLauncher复制到测试镜像的hotspot/gtest/<variant>目录;在 Windows 上还会额外复制 MSVCR/VCRUNTIME 运行时 DLL 及调试符号(PDB)。这说明:
- 测试产物是独立于
libjvm主库的 gtest 专用库 + 启动器,运行测试时使用gtestLauncher; - 测试用例的发现完全依赖 test/hotspot/gtest/ 下
test_*.cpp的文件命名约定(即 4.2 节所述构建系统要求)。
运行层面,指南明确提到 GoogleTest 的既有能力都可以用(通过 gtestLauncher 传入 GoogleTest 过滤参数即可只跑特定测试组或测试名),并建议测试作者用随机顺序、单独运行等方式验证测试的隔离性与可重复性(这也是 TODO 中列出的待补充专题)。
八、总结
doc/hotspot-unit-tests.md 虽然篇幅不长,却浓缩了 HotSpot 原生测试的完整价值体系:轻量、隔离、原子、可重复、信息充分、真正测试而非访问、检查就近七项属性,加上断言选择、参数顺序、浮点/字符串专用宏、命名分组、friend 类、death test 限制、标志守卫与恢复等一系列可落地的规则。配合 test/hotspot/gtest/ 中 240+ 个test_*.cpp文件(覆盖 gc、runtime、compiler、oops、memory、logging、cds、nmt、utilities 以及 aarch64/x86/riscv/s390 等平台)与 test/hotspot/gtest/unittest.hpp 的宏实现,任何人都能快速写出符合 HotSpot 团队规范、可维护、可调试的原生单元测试。
【免费下载链接】jdkJDK main-line development https://openjdk.org/projects/jdk项目地址: https://gitcode.com/GitHub_Trending/jd/jdk
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考