OpenJDK HotSpot 原生单元测试开发指南:基于 GoogleTest 的 TEST / TEST_VM / TEST_OTHER_VM 实践全解

发布时间:2026/9/12 7:28:29
OpenJDK HotSpot 原生单元测试开发指南:基于 GoogleTest 的 TEST / TEST_VM / TEST_OTHER_VM 实践全解
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 引入了 GoogleTestC 单元测试框架来编写 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而非ASSERTEXPECT非致命失败与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 ULPUnit in the Last PlaceEXPECT_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编译器日志子系统 →CompilerLoggingG1 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或foohumongous_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 架构组织测试的实例。五、杂项规范Miscellaneous5.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) returnrequires注释在TEST_OTHER_VM和TEST_VM_ASSERT*中传递测试专用标志的能力是需要的系统测试、回归测试等需要以特定配置运行完整 JVM例如选择 Serial GC但尚未实现计划在后续版本中加入。目前的临时 workaround 是如果测试依赖某个标志值应在测试最开头加if (!flag) { return; }守卫并在测试宏正上方加一段类似 jtregrequires指令的注释。指南明确指出必须遵循这个模式因为这样便于日后统一找到这些测试在标志传递设施实现后一次性更新。仓库中的真实范例正是 test/hotspot/gtest/gc/g1/test_g1IHOPControl.cppTEST_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 用AutoSaveRestoresize_t FLAG_GUARD(MinHeapSize);等一次性保存并恢复MinHeapSize、InitialHeapSize、MaxHeapSize、MaxNewSize、MinHeapDeltaBytes、NewSizetest/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 测试不依赖 JVMTEST_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 处于不可工作状态反正每次都是全新 JVMTEST_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 编译出的libjvmgtest 版与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),仅供参考