OceanBase 单元测试实战指南:编写、构建与运行 GoogleTest 用例 OceanBase 单元测试实战指南编写、构建与运行 GoogleTest 用例【免费下载链接】oceanbaseOceanBase is the unified distributed database for the AI era — open-source, multi-model, one engine for your most demanding workloads.项目地址: https://gitcode.com/GitHub_Trending/oc/oceanbase本指南以 OceanBase 官方文档 docs/docs/en/unittest.md 为主线结合仓库内真实的构建脚本、CMake 配置与测试用例源码系统讲解如何在 OceanBase 开源仓库中编写、构建和运行单元测试既覆盖全部用例一键构建 ctest 批量执行的完整流程也涵盖单个用例单独构建与调试的日常开发路径并深入剖析 GoogleTest 测试宏、CMake 注册机制与 CI 集成方式。读完本文你将具备在本地为 OceanBase 新增并运行一个单元测试的完整实战能力。一、认识仓库中的两个单元测试目录OceanBase 是一个大型 C 分布式数据库项目其单元测试代码按被测对象分为两个目录目录被测代码定位unittestsrc 下的核心代码覆盖 SQL 引擎、存储引擎、事务、日志、RootService 等主要模块deps/oblib/unittestdeps/oblib/src 下的公共基础库覆盖 oblib 提供的通用组件此外根 CMakeLists.txt 中还展示了与单元测试并列的另外两类测试目标mittest位于 mittest包含 multi_replica、simple_server 等偏集成/多副本场景的用例与sensitive_test它们仅在开启OB_BUILD_CLOSE_MODULES时被引入构建系统。日常迭代中unittest与deps/oblib/unittest是最常用的两层测试体系。二、构建并运行全部单元测试2.1 初始化并构建 Debug 工程官方文档给出的标准做法是先用build.sh初始化并构建一个 debug 模式的工程再单独进入 build 目录下的 unittest 子目录进行编译。bash build.sh --init --make # 初始化依赖并以 debug 模式构建工程--init会调用 deps/init/dep_create.sh 拉取编译所需的三方依赖--make表示构建完成后立即执行 make不带 BuildType 时默认是 debug 模式生成的构建目录为build_debug详见 build.sh 的用法说明与prepare_build_dir逻辑。如需指定其他构建类型可参考./build.sh debug --make -j24 # debug 模式24 并发 ./build.sh release --init # release 模式仅初始化不编译2.2 单独构建 unittest关键点在于默认构建 OceanBase 工程时并不会构建单元测试。根 CMakeLists.txt 中的两个开关决定了这一行为cmake_dependent_option( OB_INCLUDE_UNITTEST Include unittest ON NOT OB_BUILD_PACKAGE OFF) option(OB_BUILD_UNITTEST OFF)OB_INCLUDE_UNITTEST默认 ON仅将 unittest 加入构建系统但不参与默认构建对应第 216 行的add_subdirectory(unittest EXCLUDE_FROM_ALL)OB_BUILD_UNITTEST默认 OFF打开后 unittest 才会成为默认构建目标的一部分对应第 209-210 行的add_subdirectory(unittest)打包场景OB_BUILD_PACKAGE下两者均被关闭产物中不包含测试代码。因此要构建测试必须显式进入 unittest 构建目录执行 makecd build_debug/unittest # 或 cd build_debug/deps/oblib/unittest make -j4 # 构建全部单元测试构建完成后即可运行run_tests.sh脚本执行所有注册的测试用例./run_tests.sh该脚本位于 unittest/run_tests.sh其实现非常精简——本质上是对仓库自带的 ctest 工具的封装#!/bin/bash TOPDIRreadlink -f \dirname $0\ CTEST_COMMAND${TOPDIR}/../../deps/3rd/usr/local/oceanbase/devtools/bin/ctest ${CTEST_COMMAND} $也就是说run_tests.sh会把所有通过add_test注册到 CTest 中的用例逐一执行并汇总结果参数可直接透传给 ctest例如./run_tests.sh -R test_chunk可过滤执行匹配的用例。三、构建并运行单个单元测试日常开发中更常用的做法是只编译、只运行某一个用例。官方文档强调要在build_debug目录而不是 unittest 子目录下执行 make以目标名构建指定用例。以test_chunk_row_store为例cd build_debug # **NOTE**: 不要进入 unittest 目录 make -j4 test_chunk_row_store find . -name test_chunk_row_store # 输出示例./unittest/sql/engine/basic/test_chunk_row_store ./unittest/sql/engine/basic/test_chunk_row_storemake case-name会直接产出对应目录下的测试二进制find用于确认可执行文件的落盘位置随后直接运行该二进制即可。由于 GoogleTest 的可执行文件天然支持--gtest_filter、--gtest_list_tests等参数你也可以在运行单个测试时做更细粒度的过滤例如./unittest/sql/engine/basic/test_ra_row_store_projector --gtest_filterRARowStore.keep_projector3.1 单个用例是如何注册进构建系统的在 unittest/sql/engine/basic/CMakeLists.txt 中可以看到test_chunk_row_store与本文示例test_ra_row_store_projector正是通过sql_unittest宏注册的#ob_unittest(test_ra_row_store) sql_unittest(test_ra_row_store_projector) sql_unittest(test_chunk_row_store) sql_unittest(test_chunk_datum_store)sql_unittest定义在 unittest/sql/CMakeLists.txt它是对底层ob_unittest的再封装额外链接了 SQL 引擎测试公共库function(sql_unittest case) ob_unittest(${ARGV}) target_link_libraries(${case} PRIVATE sql_ut_base) endfunction()而ob_unittest定义在 unittest/CMakeLists.txt它完成了可执行文件的生成、GoogleTest 测试注册与超时保护function(ob_unittest case) if(ARGC EQUAL 1) add_executable(${case} ${case}.cpp) else() add_executable(${ARGV}) endif() if (case MATCHES ^test_.*) add_test(${case} ${case}) set_tests_properties(${case} PROPERTIES TIMEOUT 300) endif() target_link_libraries(${case} PRIVATE -Wl,--whole-archive mock_di -Wl,--no-whole-archive oceanbase gtest gmock) target_include_directories(${case} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/unittest ${CMAKE_SOURCE_DIR}/mittest ${CMAKE_SOURCE_DIR}/deps/oblib/unittest) endfunction()几个值得注意的实现细节凡文件名以test_开头的用例都会通过add_test注册进 CTest并设置 300 秒超时TIMEOUT 300这正是run_tests.sh能够批量调度它们的原因每个测试都会链接oceanbase主库、gtest/gmock框架以及用于依赖打桩的mock_di以--whole-archive方式整体链接避免 mock 符号被裁剪头文件搜索路径统一包含unittest、mittest、deps/oblib/unittest方便测试代码复用各层的公共测试工具。四、如何编写一个单元测试4.1 文件命名与注册OceanBase 的单元测试文件统一采用test_xxx.cpp的命名规范。编写新用例的步骤是在对应模块的 unittest 子目录中创建test_xxx.cpp将文件名或额外源文件登记到该目录的CMakeLists.txt中例如在 unittest/sql/engine/basic/CMakeLists.txt 追加一行sql_unittest(test_xxx)在文件中包含 GoogleTest 头文件并编写测试体。4.2 固定骨架main 函数作为 C 项目OceanBase 使用 GoogleTestgtest作为单元测试框架。每个测试文件都需要包含#include gtest/gtest.h并提供如下形式的 main 函数int main(int argc, char **argv) { testing::InitGoogleTest(argc, argv); return RUN_ALL_TESTS(); }testing::InitGoogleTest负责解析--gtest_*命令行参数RUN_ALL_TESTS()则遍历并执行所有注册的测试用例返回失败用例数量。实际仓库中的 main 通常会再初始化日志见 unittest/sql/engine/basic/test_ra_row_store_projector.cppint main(int argc, char **argv) { oceanbase::common::ObLogger::get_logger().set_log_level(INFO); testing::InitGoogleTest(argc, argv); return RUN_ALL_TESTS(); }4.3 用 TEST 宏组织用例用 ASSERT_/EXPECT_ 断言结果GoogleTest 的核心组织单元是TEST(TestSuiteName, TestName)宏它声明一个测试函数第一参数是测试套件suite名第二参数是测试名。断言宏分为两类ASSERT_XXX断言失败时立即终止当前测试函数EXPECT_XXX断言失败时记录失败但不终止继续执行后续断言。官方文档以test_ra_row_store_projector.cpp为例展示了完整写法。该文件位于 unittest/sql/engine/basic/test_ra_row_store_projector.cpp包含两个测试RARowStore.keep_projector与RARowStore.alloc_project_fail。下面剖析第一个用例TEST(RARowStore, keep_projector) { ObRARowStore rs(NULL, true); ASSERT_EQ(OB_SUCCESS, rs.init(100 20, OB_SYS_TENANT_ID)); const int64_t OBJ_CNT 3; ObObj objs[OBJ_CNT]; ObNewRow r; r.cells_ objs; r.count_ OBJ_CNT; int64_t val 0; for (int64_t i 0; i OBJ_CNT; i) { objs[i].set_int(val); val; } int32_t projector[] {0, 2}; r.projector_ projector; r.projector_size_ ARRAYSIZEOF(projector); ASSERT_EQ(OB_SUCCESS, rs.add_row(r)); for (int64_t i 0; i OBJ_CNT; i) { objs[i].set_int(val); val; } r.projector_size_--; ASSERT_NE(OB_SUCCESS, rs.add_row(r)); r.projector_size_; ASSERT_EQ(OB_SUCCESS, rs.add_row(r)); const ObNewRow *rr NULL; ASSERT_EQ(OB_SUCCESS, rs.get_row(1, rr)); ASSERT_TRUE(NULL ! rr-projector_); ASSERT_NE(projector, rr-projector_); ASSERT_EQ(ARRAYSIZEOF(projector), rr-get_count()); ASSERT_EQ(OBJ_CNT, rr-get_cell(0).get_int()); ASSERT_EQ(OBJ_CNT 1, rr-cells_[1].get_int()); ASSERT_EQ(OBJ_CNT 2, rr-get_cell(1).get_int()); // only fill cells, projector_ unchanged. ASSERT_EQ(OB_SUCCESS, rs.get_row(0, r)); ASSERT_EQ(projector, r.projector_); ASSERT_EQ(0, r.get_cell(0).get_int()); ASSERT_EQ(1, r.cells_[1].get_int()); ASSERT_EQ(2, r.get_cell(1).get_int()); }该用例针对 SQL 引擎的行存类ObRARowStore实现位于 src/sql/engine/basic/ob_ra_row_store.h验证其投影projector保持语义通过ASSERT_EQ(OB_SUCCESS, ...)断言初始化与添加行成功通过临时把projector_size_减一构造非法输入并用ASSERT_NE(OB_SUCCESS, ...)断言非法输入会被拒绝最后用ASSERT_NE(projector, rr-projector_)验证行存储在内部对投影数组做了拷贝而非直接持有调用方指针。第二个用例alloc_project_fail则演示了故障注入式测试——自定义一个永远分配失败的ObEmptyAlloc分配器继承自ObIAllocatoralloc直接返回 NULL从而验证内存分配失败路径的返回值符合预期class ObEmptyAlloc : public ObIAllocator { void *alloc(const int64_t) override { return NULL; } void *alloc(const int64_t, const ObMemAttr ) override { return NULL; } void free(void *ptr) override { UNUSED(ptr); } }; TEST(RARowStore, alloc_project_fail) { ObEmptyAlloc alloc; ObRARowStore rs(alloc, true); ASSERT_EQ(OB_SUCCESS, rs.init(100 20, OB_SYS_TENANT_ID)); // ... 构造一行含 3 个 cell、投影 {0, 2} 的行 ... ASSERT_EQ(OB_ALLOCATE_MEMORY_FAILED, rs.add_row(r)); }这种自定义 Mock 分配器 断言错误码的写法在 OceanBase 测试中非常典型所有函数都以OB_SUCCESS或具体错误码作为返回值契约测试只需断言返回码即可覆盖正常路径与异常路径。关于TEST、ASSERT_*、EXPECT_*的更多高级用法如参数化测试、fixture、EXPECT_THAT等可参考 GoogleTest 官方文档。五、Unittest 在 GitHub CI 中的运行方式在 OceanBase 的 GitHub 工作流中Pull Request 合并前必须通过 CI 校验。CI 中的Farm环节会执行两类测试mysql test面向 SQL 语义的端到端测试相关方法论可参考 docs/docs/en/mysqltest.mdunittest即本文所述的 GoogleTest 用例。在 PR 页面点击检查项后的Details链接即可进入 Farm 的详细执行页面查看每个测试步骤的状态与日志进入 unittest 步骤可以查看用例集合的调度与执行情况失败用例会在此处以红色标出便于开发者定位并修复对本地开发者而言上述 CI 中的 unittest 与run_tests.sh调度的其实是同一套用例——只要本地用例能通过CI 中对应环节也就具备了通过的基础。六、调试与进一步阅读单元测试失败时建议结合 docs/docs/en/debug.md 中的调试手段如 gdb、日志级别调整定位问题测试中也可以通过修改set_log_level调整 OceanBase 日志输出量编写用例时应遵守仓库的 docs/docs/en/coding-convention.md 与 docs/coding_standard.md尤其是OB_SUCCESS错误码检查、ObObj/ObNewRow等核心类型的正确用法若用例涉及日志与内存观测可参考 docs/docs/en/logging.md 与 docs/docs/en/memory.md 理解 OceanBase 的日志与内存模型从而写出更贴近真实运行环境的断言。总结OceanBase 的单元测试体系以 GoogleTest 为框架遵循test_xxx.cpp文件 CMake 注册 ASSERT_/EXPECT_断言 错误码校验的固定范式本地通过build_debug目录下的make按需编译单个用例通过run_tests.shctest 封装批量执行全部用例在 CI 侧同一套用例由 Farm 统一调度作为 PR 合入的必经关卡。掌握这套流程你就能在贡献 OceanBase 时为任意模块快速补充高质量的单元测试。【免费下载链接】oceanbaseOceanBase is the unified distributed database for the AI era — open-source, multi-model, one engine for your most demanding workloads.项目地址: https://gitcode.com/GitHub_Trending/oc/oceanbase创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考