C++单元测试实战:GoogleTest从入门到项目落地
如果你写过一阵子C迟早会碰到这样的场景手头一个排序算法改了实现心里没底不敢重构接手老代码想动一个函数却不知道动完会不会把别的地方搞炸。这时候单元测试就是你的安全网而gtestGoogleTest几乎就是C单元测试的事实标准。这篇文章不整虚的直接从环境搭建到断言用法、生命周期、参数化、命令控制到踩坑实录带你快速上手这套工具尽快在你自己的项目里落地跑起来。不管你是学生、刚转C的后端开发还是想给老项目补测试的维护者读完之后动手跟一遍就能体会到“代码敢改了”是种什么感觉。1. 先把单元测试这件事想清楚1.1 为什么偏偏是单元测试很多C开发者的现状是代码写完跑一遍主程序输入几组数据看到输出对就收工。这种“手动冒烟测试”在工程里远远不够。真正常见的翻车点往往藏在边界条件、异常输入、并发竞争这些平时不会去手点的路径里等合入主干、上了流水线甚至到了线上才爆出来。单元测试的字面意思是“以单元为粒度测试”在C里这个单元通常是一个类、一个函数或者一组有强关联的接口。它解决的核心问题有三个。第一是回归改动代码后跑一遍能告诉你“原来好的东西有没有坏掉”第二是定位某一个测试挂了直接告诉你哪个函数、哪个用例、哪行断言出的问题不用开着调试器漫无目的地断点第三是文档一组写得很清晰的测试就是用户手册新人看一眼就知道这个接口该怎么用、边界是什么。不过我也要泼一点冷水单元测试不是银弹。它测不出多个模块集成后的协作问题也测不出性能瓶颈和分布式下的复杂故障。它的价值在于把能自动化验证的逻辑锁死让重构的胆子变大让bug在下游和线上出现的概率变小。所以如果你准备开始写测试先把“这个测试到底在保护什么”想清楚比急着敲代码更重要。1.2 gtest凭什么值得学C的测试框架其实不少CppUnit、Boost.Test、Catch2、doctest各有各的拥趸。但gtest是目前综合生态最好、资料最多、团队里最容易被认可的选择理由也很实在。首先是它由Google维护常年在Chromium等大规模项目里自用稳定性和兼容性经过大量实战验证。其次是断言非常丰富从最基础的相等、布尔判断到字符串匹配、浮点数精度、异常抛出再到死亡测试验证程序在特定输入下确实会崩溃基本你想做的检查它都有现成宏。第三它提供了测试夹具Test Fixture和参数化测试机制能让大量相似用例共用一套逻辑代码写起来不啰嗦。第四配合GoogleMockgtest还可以做C对象的mock后面要测模块间交互、第三方依赖的时候特别管用。和Boost.Test比gtest不用引入一堆Boost头文件编译负担小和Catch2比gtest的断言宏风格更接近传统C习惯老开发者上手更快。而且国内各大互联网公司的C部门基本已经把它当成默认基建面试聊到测试体系时说“我会gtest”也远比“我听说过xxx”更有说服力。所以不管你最终选择哪个框架先学会gtest绝对不亏。1.3 哪些项目适合用gtest理论上凡是能用CMake管理的C项目都能用gtest。但实际上不同类型项目的落地方式差别很大。我大概分几类说一下。如果你在写一个独立的算法库、工具库那是gtest最理想的场景。把对外接口一个个用测试套住后续重构优化都不用提心吊胆。如果你在维护一个大型业务系统比如服务器后端、客户端gtest适合放在独立的测试工程里针对核心业务模块做单元测试而不是把整个系统全部测一遍——那种体量应该交给集成测试。如果你的项目跑在嵌入式设备或者在某些特殊平台上gtest也能用但要注意资源的限制。gtest本身是个头文件加一个编译单元裁剪之后可以用只是很多高级特性比如死亡测试里的fork在某些交叉编译环境下表现不一样具体后面讲坑的时候说。最后一种情况是给遗留老代码补测试。很多前辈说“老代码没法测”其实不是没法测而是缺乏抓手。gtest可以让你从最独立的函数开始一层层把行为锁住再慢慢去改里面的实现。这种渐进式引入的路子我见过很多团队走得很好关键是别想一口气全部覆盖。2. 环境准备三步把gtest请进项目2.1 获取gtest的几种姿势引入gtest的方式有很多种我挑最靠谱的三种说按推荐程度排序。第一种是直接用CMake的FetchContent模块在构建时自动下载指定版本的gtest源码并编译。这个方式对版本控制最友好整个依赖逻辑写在CMakeLists.txt里团队其他人拉下来就能跑不需要额外装全局库。在公司的CI环境里只要机器能访问外网或者搭了内部代理就能复现。第二种是把gtest源码直接以子模块或副本的形式塞进项目比如放进third_party/gtest目录再用add_subdirectory引入。这个方法适合需要离线构建、或者对依赖版本做强管控的团队缺点是仓库会变大升级依赖时要手动操作。第三种是通过系统包管理器直接安装比如Ubuntu下执行sudo apt install libgtest-dev。这种方式在本地快速体验时最方便但很多发行版里的gtest版本偏老而且安装后的库文件路径、CMake配置文件在部分系统上不标准直接target_link_libraries可能链接不到反而要多折腾。所以我更建议用前两种尤其是FetchContent下面重点讲这个。2.2 CMake集成最小示例这里我给一个可以直接抄走的CMakeLists.txt结构cmake_minimum_required(VERSION 3.14) project(MyAwesomeProject CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 引入gtest include(FetchContent) FetchContent_Declare( googletest URL https://github.com/google/googletest/archive/refs/tags/v1.14.0.tar.gz ) FetchContent_MakeAvailable(googletest) # 你的业务库 add_library(my_math my_math.cpp) # 启用测试 enable_testing() # 测试可执行文件 add_executable(math_test math_test.cpp) target_link_libraries(math_test PRIVATE my_math gtest_main) include(GoogleTest) gtest_discover_tests(math_test)这里有几个点我特意想解释一下很多新手直接抄不知道背后含义。enable_testing()是在当前目录启用CTest它是CMake自带的测试驱动工具能统一管理多个测试可执行文件。gtest_main这个库很关键它包含了main函数你的测试文件里就不用自己写了。如果你的项目里有多个测试可执行文件每个只需要链接gtest_main就能直接运行输出结果。gtest_discover_tests(math_test)这行是做测试自动注册的。编译完成后执行ctest它会自动去发现并逐个运行math_test里的所有用例这样在CI流水线里只要跑一个ctest命令就能拿到所有测试的通过率和失败详情。如果不写这行你也可以直接执行编译出来的./math_test来跑测试效果是一样的只是CTest这边少了一层统一调度。2.3 第一个能跑的测试代码假设你有一个最简单的整数相加函数// my_math.h #pragma once int add(int a, int b);// my_math.cpp #include my_math.h int add(int a, int b) { return a b; }对应的测试文件// math_test.cpp #include gtest/gtest.h #include my_math.h TEST(AddTest, PositiveNumbers) { EXPECT_EQ(add(2, 3), 5); } TEST(AddTest, NegativeNumbers) { EXPECT_EQ(add(-2, -3), -5); }编译运行mkdir build cd build cmake .. cmake --build . ./math_test如果一切正常你会看到类似这样的输出[] Running 2 tests from 1 test suite. [----------] Global test environment set-up. [----------] 2 tests from AddTest [ RUN ] AddTest.PositiveNumbers [ OK ] AddTest.PositiveNumbers (0 ms) [ RUN ] AddTest.NegativeNumbers [ OK ] AddTest.NegativeNumbers (0 ms) [----------] 2 tests from AddTest (0 ms total) [----------] Global test environment tear-down [] 2 tests from 1 test suite ran. (1 ms total) [ PASSED ] 2 tests.看到PASSED就是成功了。这里TEST(AddTest, PositiveNumbers)定义了一个名为AddTest的测试套件Test Suite里面有一个测试用例Test Case叫PositiveNumbers。在gtest的输出里套件名和用例名用点号拼接形成了唯一的测试全名AddTest.PositiveNumbers。这个全名在后面用--gtest_filter做过滤时非常有用。3. 断言体系把“测试”变成“检查”3.1 EXPECT_ 与 ASSERT_ 的区别gtest最核心的语法就是断言。每个断言的宏都分两套EXPECT_*和ASSERT_*。区别一句话说清楚ASSERT_*失败的话当前这个测试用例立即中断直接判失败EXPECT_*失败的话记录下来当前断言失败但这个测试用例还会继续往后跑。可能有人会问那是不是全用EXPECT_就好了也不是。这取决于你检查的上下文。比如你测一个函数返回了一个指针如果这个指针是nullptr后面的步骤全都没有意义那你应该用ASSERT_NE(ptr, nullptr)让用例及时终止避免后面空指针解引用产生难以追踪的崩溃。而如果只是检查几个彼此逻辑独立的值比如一个配置对象里多个字段是否都正确那就用EXPECT_EQ一个接一个查一次跑完能看到所有失败项不用一遍遍重试。我个人的原则是前置条件用ASSERT_*属性校验用EXPECT_*。前置条件一旦不满足后续逻辑没必要继续属性校验的目标是收集尽可能多的失败信息方便一次修复所有问题。3.2 常用断言速查下面这份表是我平时用得最多的断言建议直接收藏场景断言说明相等/不等EXPECT_EQ / EXPECT_NE判断值相等或不等打印时带实际值布尔判断EXPECT_TRUE / EXPECT_FALSE判断表达式为真或假指针EXPECT_EQ / EXPECT_NE比较指针是否相等注意不要用EXPECT_EQ去比较浮点数浮点数近似相等EXPECT_FLOAT_EQ / EXPECT_DOUBLE_EQ使用内置的ULP级容差比较浮点数浮点数可控容差EXPECT_NEAR(val1, val2, abs_err)自定义绝对误差范围字符串相等EXPECT_STREQ / EXPECT_STRNE比较C风格字符串内容不能用EXPECT_EQ字符串子串EXPECT_STRCASEEQ不区分大小写比较ASCII字符串抛出异常EXPECT_THROW(expr, ExceptionType)表达式必须抛出指定异常类型不抛异常EXPECT_NO_THROW(expr)表达式不能抛任何异常任意异常EXPECT_ANY_THROW(expr)表达式必须抛出任意异常死亡测试EXPECT_DEATH(expr, 正则)表达式必须导致进程非正常退出有两个细节我要重点提醒。第一字符串比较这点是C新手最常见的坑。EXPECT_EQ(str1, str2)在char*场景下比较的是指针地址而不是内容所以大概率会得出“明明看起来一样却失败”的诡异结果。正确做法是用EXPECT_STREQ。如果你用的是std::string那EXPECT_EQ是可以的因为它会调用operator重载去比较内容但为了让失败信息更可读我通常还是愿意显式说明意图。第二浮点数不能直接比相等。计算机里的浮点数二进制表示决定了0.1加0.2的结果并不是精确的0.3。如果你在测试里写EXPECT_EQ(add(0.1, 0.2), 0.3)大概率会得到一个红色失败。这是所有语言都会遇到的问题不是gtest的bug。正确的姿势是用EXPECT_NEAR或者EXPECT_DOUBLE_EQ具体用哪个看你需要的精度。3.3 自定义失败信息断言宏后面可以用追加一串自定义输出这个在复杂场景里特别好用TEST(FooTest, Bar) { int result someFunction(42); EXPECT_EQ(result, 100) someFunction(42) 返回值异常实际得到 result; }当断言失败时gtest会把这串输出和断言自带的诊断信息一起打印出来。如果你在一个循环里跑多组数据那这串输出里最好带上当前循环的下标、输入参数和期望值TEST(FooTest, LoopCases) { std::vectorint inputs {1, 2, 3, 4, 5}; std::vectorint expects {10, 20, 30, 40, 50}; for (size_t i 0; i inputs.size(); i) { EXPECT_EQ(transform(inputs[i]), expects[i]) 下标 i 输入 inputs[i] 期望 expects[i]; } }没有这行追加输出你只会看到“期望30实际得到25”然后还得自己去数这是哪组数据有了日志测试失败的一瞬间你就能定位是哪一轮循环、什么入参出的问题。这个小习惯能帮你省下大量排查时间。4. 测试夹具共享数据和生命周期4.1 为什么需要夹具很多测试用例需要准备一些公共数据。比如测一个队列每个用例都得先建一个Queue对象往里面塞一些元素再调用要测的方法。如果每个用例都把这些代码重复一遍测试文件会变得冗长又难维护。gtest提供的测试夹具机制就是专门解决这个问题的。夹具类需要继承::testing::Test然后在类里定义好要共享的成员变量和辅助方法。通过TEST_F宏定义的用例会自动获得一个夹具实例每个用例执行前构造成员变量执行后自动析构。这样每个测试用例的起点状态都是一样的互不干扰。4.2 TEST_F 的基本写法下面是一个最小但完整的夹具示例。假设我们有一个简单的IntStack类// int_stack.h #pragma once #include vector class IntStack { public: void push(int v) { data_.push_back(v); } int pop() { int v data_.back(); data_.pop_back(); return v; } bool empty() const { return data_.empty(); } size_t size() const { return data_.size(); } private: std::vectorint data_; };对应的测试文件#include gtest/gtest.h #include int_stack.h class IntStackTest : public ::testing::Test { protected: void SetUp() override { // 每个用例执行前都会调用这里 stack.push(1); stack.push(2); stack.push(3); } // void TearDown() override {} IntStack stack; }; TEST_F(IntStackTest, PopReturnsInReverseOrder) { EXPECT_EQ(stack.pop(), 3); EXPECT_EQ(stack.pop(), 2); EXPECT_EQ(stack.pop(), 1); } TEST_F(IntStackTest, EmptyAfterAllPops) { stack.pop(); stack.pop(); stack.pop(); EXPECT_TRUE(stack.empty()); }注意第一个测试里我把三个元素都pop掉了第二个测试里我又pop了三次。两个用例之间完全互不干扰因为每个TEST_F执行前gtest都会重新创建一个新的IntStackTest对象然后调用SetUp把栈填满。所以第二个测试执行的时候栈里又变回了三个元素。这就是“每个用例独立副本”的意义——测试之间不会因为共享了可变状态而互相踩脚。4.3 构造函数 vs SetUp 的选择如果你是C11之后入门的开发者可能会觉得奇怪为什么gtest夹具不用构造函数来初始化非要搞一个SetUp虚函数其实在现代C风格里你完全可以在夹具类的构造函数里写初始化逻辑效果一样。但SetUp也有它的价值当初始化过程中出现失败时SetUp里触发ASSERT_*可以让gtest正确感知“当前用例失败”而构造函数如果抛异常或断言失败处理起来不如SetUp自然。另外TearDown对应析构函数但如果TearDown里也需要断言清理结果那就只能用TearDown而不能依赖析构函数。我的习惯是简单的成员变量初始化直接用构造函数或在声明处初始化需要在初始化里做资源创建、文件读取并且想验证这些过程是否成功时用SetUp。两者可以混用但要清晰地知道每个用例执行时先走构造函数再走SetUpTearDown先于析构函数执行。5. 参数化测试一套代码喂多组数据5.1 手动参数化的痛点测试写多了就会遇到一个很自然的诉求同一个测试逻辑我想用多组不同输入去验证。最蠢的办法是写多个TEST比如SortTest.SortedInput、SortTest.ReversedInput、SortTest.DuplicatedInput每个用例里的断言逻辑几乎一模一样。这样写虽然直白但重复代码多后续如果要改一点校验逻辑所有用例都得跟着改。用for循环包一组断言也不好。第一断言失败时虽然能通过追加日志指出是哪一组数据挂了但整个用例的结果永远是“第一个失败的断言”其他的数据点还得等修复后再跑第二循环里的多个数据点被绑定在一个测试用例里缺少独立性一挂全挂粒度太粗。gtest的参数化测试机制恰好就为“一套用例多组参数”而生。5.2 TEST_P INSTANTIATE_TEST_SUITE_P 的标准姿势参数化测试的经典写法分三步。第一步定义一个夹具类继承::testing::TestWithParamT其中T是你想要的参数类型。第二步用TEST_P宏编写测试用例在用例里通过GetParam()获取当前参数。第三步用INSTANTIATE_TEST_SUITE_P宏实例化一组具体参数。来一个完整例子。假设我们对一个字符串翻转函数写参数化测试#include gtest/gtest.h #include string #include algorithm class StrRevTest : public ::testing::TestWithParamstd::pairstd::string, std::string { }; TEST_P(StrRevTest, ReversesCorrectly) { auto param GetParam(); std::string input param.first; std::string expected param.second; std::reverse(input.begin(), input.end()); EXPECT_EQ(input, expected); } INSTANTIATE_TEST_SUITE_P( StrRevParams, StrRevTest, ::testing::Values( std::make_pair(, ), std::make_pair(a, a), std::make_pair(abc, cba), std::make_pair(hello world, dlrow olleh) ));编译运行后你会看到这样一个测试套件StrRevParams/StrRevTest.ReversesCorrectly/0、/1、/2、/3。每个下标对应Values里的第几组参数。这是gtest自动生成的编号实例名。如果你想给每个参数组一个更有意义的名字可以用INSTANTIATE_TEST_SUITE_P的第四个参数传一个自定义命名函数。参数化的类型也可以复杂。除了基本类型和STL容器你也可以传自定义结构体。只要这个类型支持流式输出即实现了operator失败信息里gtest就能打印出当前参数内容否则会遇到编译错误。这也是一个需要额外留意的地方。5.3 参数化命名与过滤如果你在跑一大堆参数化测试时某个用例挂了gtest输出的测试名会带着编号比如StrRevParams/StrRevTest.ReversesCorrectly/2。但这个编号本身不够直观——你想知道它是哪一组参数还得回过头去数Values列表里的位置。解决方法是自定义实例名用::testing::PrintToString加一个命名函数struct StrRevTestPrint { std::string operator()(const ::testing::TestParamInfoStrRevTest::ParamType info) const { return input_ ::testing::PrintToString(info.param.first); } }; INSTANTIATE_TEST_SUITE_P( StrRevParams, StrRevTest, ::testing::Values(...), StrRevTestPrint );这样测试名会变成StrRevParams/StrRevTest.ReversesCorrectly/input_abc一眼就能看出是哪个输入。自定义命名函数还有一个作用PrintToString只能打印流式输出支持的类型如果你的自定义结构体没有operator但你又想生成名字那么这里才是真正需要处理的地方。另外在Windows和部分文件系统上测试名字里如果有/或\运行出来会别扭所以自定义名字里尽量用字母、数字、下划线。6. 运行控制与CI集成6.1 命令行参数gtest的可执行文件本身就是一个命令行工具内置了一堆运行控制参数。掌握这些参数你在日常调试和CI排障时效率能提升一个档次。最常用的是--gtest_filter格式是Pattern1:Pattern2:-Pattern3支持通配符*和?。比如我只想跑AddTest这个套件可以执行./math_test --gtest_filterAddTest.*如果想跑多个套件用冒号分隔./math_test --gtest_filterAddTest.*:StrRevTest.*如果想排除某个套件前面加负号./math_test --gtest_filter-FloakyTest.*这些过滤规则也支持?匹配单个字符比如--gtest_filter?ddTest.*会匹配AddTest、OddTest等。在做故障定位时只跑一个或多个相关的测试能省掉大量无关输出。--gtest_list_tests会把当前可执行文件里的所有测试套件和用例列出来不实际执行方便你确认命名和过滤规则。--gtest_repeat10会把所有选中的测试重复跑10次对于排查偶现失败俗称flaky test非常有用。配合--gtest_shuffle随机打乱执行顺序还能帮助发现测试间的隐藏依赖这类问题最隐蔽也最值得警惕。6.2 输出XML/JSON报告与CI集成在CI流水线里你不可能每次都人肉去看控制台输出。gtest内置了报告输出能力./math_test --gtest_outputxml:test_report.xml它会生成一份标准的JUnit XML格式报告。GitLab CI、Jenkins、GitHub Actions这些平台都能直接解析这种格式把测试结果渲染成漂亮的UI失败时还会在对应代码提交下直接列出失败用例。新版gtest还支持输出JSON./math_test --gtest_outputjson:test_report.jsonJSON格式包含的信息更丰富适合后续写脚本做自定义分析。我在实际项目里的做法是在每个测试可执行文件上都封装一层CTest然后在CI脚本里执行ctest --output-on-failure。这样既拿到了标准化的退出码0代表全部通过非0代表有失败也能让CTest把失败用例的详细输出打印到流水线日志里。CI上跑测试最怕的就是“全绿但没人看”而gtest的失败信息足够显眼红色失败一眼就能揪出来。6.3 禁用与临时跳过还有一种常见场景某个测试用例当前挂了但你不想把它删掉想先把其他用例跑完。gtest支持两类处理方式。第一是在测试名里加DISABLED_前缀比如TEST(AddTest, DISABLED_OverflowTest) { // ... }gtest默认会跳过所有带DISABLED_前缀的测试并在输出里提示忽略了几个。第二是通过命令行指定./math_test --gtest_filterAddTest.*-AddTest.OverflowTest一个是永久性禁用一个是运行时过滤用途不同。我建议能不用DISABLED_就不用因为它很容易变成“挂在那里的僵尸测试”没人记得去恢复。真要禁用请在测试代码边上注明原因、责任人、预计恢复时间。7. 常见问题与避坑实录7.1 链接错误undefined reference to testing::*刚上手frequently遇到的第一堵墙就是链接错误。明明代码看着没有问题结果编译一跑满屏的undefined reference totesting::Test::Test()、testing::internal::GetBoolAssertionFailureMessage这类报错。这种问题九成是gtest库没有正确链接。确认两件事第一你的测试目标是否添加了gtest_main或gtest库。如果只用了gtest_main它会自带main函数并自动链进gtest如果你非要自己写main函数那就得链gtest而不是gtest_main。第二CMake里target_link_libraries的库顺序在静态库场景下也有讲究。链接器解析静态库是按顺序从左往右找符号的如果你的测试代码在后面的目标文件里引用了前面的静态库符号就找不到了。用CMake的target_link_libraries传一个目标它通常会自己处理好依赖顺序所以尽量别手写-l参数。如果你用的是系统自带的libgtest-dev还可能遇到另一种情况gtest在较新版本里要求链接pthread所以编译命令可能要加-lpthread。而CMake通过FetchContent编译出来的版本一般不需要手动处理这个。7.2 ASSERT_ 用在非void函数中导致编译错误ASSERT_*宏在失败时执行的是return;所以在非void函数里直接使用编译器会报“not all control paths return a value”。这个错误很迷惑尤其当你的测试辅助函数返回bool或某个对象时。解决办法有几种。第一种是把辅助函数的返回类型改成void或者让那段逻辑直接内联到TEST体里。第二种是改用EXPECT_*因为它不会直接return失败后会继续往下走。如果你必须在辅助函数里做前置条件检查并且希望失败就中断最干净的做法是让辅助函数返回bool再在主测试里用ASSERT_TRUE(helper())包一层这样return发生在TEST函数体内部类型匹配。这个坑几乎每个gtest新手都会踩一遍写代码时注意一下就好。7.3 测试间共享数据污染gtest的测试用例默认是相互独立的但如果你在测试代码里用了全局变量、静态变量或者单例对象那“独立”就变成了假象。比如你有一个全局的日志级别配置某个用例把它改成了DEBUG另一个用例假设它还是默认的INFO如果先跑了前者再跑后者测试可能就挂了但单独跑后者的用例它又是绿的。这种由执行顺序决定成败的测试最让人头大。解决办法是测试代码里尽量不要依赖全局可变状态如果必须依赖请在SetUp里把这些变量重置到已知初始值。另外--gtest_shuffle配合--gtest_repeat可以帮你发现这类问题。当测试用随机顺序跑就不稳时基本可以断定存在共享状态污染。7.4 中文路径和文件名问题在Windows平台上如果项目路径或者测试文件路径包含中文字符gtest可能会有各种奇怪的表现。轻则编译报错重则运行时找不到da ta文件、断言失败信息乱码。gtest的源代码对Unicode路径的支持是逐步完善的老版本确实存在一些坑。我的建议是给自己省心把所有C项目路径统一成纯英文包括构建目录和缓存目录。这是Windows下玩C的一项基本素养不只是给gtest用很多其他工具链对中文路径的兼容性都一言难尽。如果你实在无法避免那就升级到最新的gtest版本并确保CMake和编译器都是较新版本能减少很多不必要的烦恼。7.5 浮点数比较的精度坑前面已经提过浮点数直接EXPECT_EQ容易踩坑我再补充一点实操经验。很多人知道用EXPECT_FLOAT_EQ但对它的原理不清楚。gtest的浮点比较不是简单地算差值的绝对值而是基于“units in the last place”的容差比较对大部分计算场景足够公平。但如果你自己知道某个算法有明确的误差允许范围比如图像处理里允许像素误差在2以内那就用EXPECT_NEAR把容差写清楚。这个断言的可读性也更好别人看测试时能明白这个精度的设计意图。还有一点不要试图依赖某一次运行偶然通过来确认浮点测试遇到偶发性的浮点断言失败先问自己两个问题——结果的方向性对不对代码里有没有未初始化的变量。7.6 死亡测试的跨平台注意事项死亡测试用来验证“程序在这种情况下应该崩溃”比如解引用空指针时程序应该终止。gtest提供EXPECT_DEATH(statement, regex)来捕获这种场景。这个能力在Linux/macOS上实现得比较稳定原理是fork一个子进程去执行statement如果子进程因为信号退出则测试通过。但在Windows等平台上EXPECT_DEATH的实现依赖不同平台的原生能力某些表达式可能无法像Linux那样完整捕获。另一个问题是死亡测试里的语句执行时的效率比普通语句差很多因为每次都要fork进程所以不要在一个测试里写一万次EXPECT_DEATH否则测试时间会非常感人。我个人建议死亡测试用在“确实需要验证崩溃行为”的场景比如防御性编程里的断言验证常规逻辑还是用普通的EXPECT_THROW之类的异常断言别过度使用。最后说一点我的个人体会。gtest本身学起来不难难的是养成写测试的习惯以及把测试写好。我见过很多开发者装完环境、跑通示例之后实际项目里还是继续裸奔。真正让测试产生价值的是那些看起来“不值得测”的小函数被一点点锁定是重构老代码时跑一遍测试获得的那点安全感。刚开始写别追求覆盖率先把最核心、最容易改坏的逻辑测起来等习惯了红绿色块交替的感觉你会发现自己写代码的方式都会慢慢变——会更注意边界、更注意可测试性、更愿意把函数拆小。这个转变比学会任何测试框架的API都值钱。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →