GoogleTest C++单元测试实战:断言、夹具、Mock与CMake集成

发布时间:2026/10/2 4:54:21
GoogleTest C++单元测试实战:断言、夹具、Mock与CMake集成 1. GoogleTest 到底是干什么的值不值得花时间学我带的几个新同事第一个月基本都栽在同一件事上——写完一个函数随手在main里printf两行看到输出对了就提交合并。三个月后需求一改谁都不敢动那段代码因为没人知道动完之后哪个分支会崩。后来我给团队立了个规矩新模块必须带单元测试。框架试用了一圈从 Boost.Test、Catch2 到 GoogleTest最后还是统一到了 GoogleTest也就是大家常说的 gtest。原因很朴素它跨平台、文档够用、生态成熟、和 CMake 结合得顺新人上手成本也低。先把话说清楚。GoogleTest 是 Google 开源的一套 C 单元测试框架跑在 xUnit 架构上核心能力是三件事写断言、组织测试用例、自动收集并运行它们。它本身不负责构建也不负责持续集成你把它理解成一个帮你把测试写规范、跑起来、还能报出漂亮结果的库就够了。配套的 gMock 是它的兄弟项目专门用来造假对象把你的被测代码从数据库、网络、硬件这些外部依赖里隔离出来。这套东西适合谁如果你是写 C 的不管你做的是后端服务、嵌入式、桌面软件还是算法库只要你的代码能编译成库或者可执行文件GoogleTest 都能用。纯小白也别慌你只要会写函数、会写if再把 CMake 的基本用法摸清楚这篇文章足够你从零开始跑通第一个测试。反过来说如果你只写一次性脚本、代码寿命不超过一周那确实没必要上框架写两行assert就够了。我特别想强调一点单元测试框架的价值不在测出 bug而在防止你已经修好的 bug 再回来。回归才是它真正省钱的地方。我统计过自己负责的一个模块引入 gtest 之后线上因为改 A 处崩 B 处引发的问题下降了大概七成这个收益远比测试本身写起来的那点时间成本高。下面我按实际落地的顺序来讲先搭环境再写第一个测试然后依次把夹具、参数化、Mock、CMake 集成、问题排查讲透。每一块我都会告诉你为什么这么选而不只是这么写能跑。2. 环境搭建三条路线按你的场景挑2.1 源码编译最通用也最不容易出幺蛾子很多人卡在第一步不是因为不会写测试而是因为 gtest 没装明白。它和普通的apt install库不太一样GoogleTest 的推荐用法是把源码拉下来用 CMake 编译成静态库然后链接进你的测试工程。这样做的好处是版本可控团队里每个人用的都是同一份代码不会出现你机器上能过、CI 上挂了这种事。拉代码很简单git clone https://github.com/google/googletest.git cd googletest cmake -S . -B build -DBUILD_GMOCKON -DCMAKE_BUILD_TYPERelease cmake --build build -j 8编译完之后build/lib目录下会出来几个文件libgtest.a、libgtest_main.a、libgmock.a、libgmock_main.a。这里有个新手特别容易搞混的点——gtest_main和gtest是两个不同的库。libgtest.a只提供宏和断言实现不提供main函数你得自己写libgtest_main.a里自带一个main它会自动帮你调用初始化并运行所有测试。什么时候用哪个我的习惯是只要没有特殊初始化需求一律链接gtest_main省掉一个手写的main代码干净。如果测试前需要读配置文件、连内存数据库之类那就用gtest自己写main。关于版本近几个大版本把最低 C 标准往上抬了1.12 之后基本要求 C14更新的版本已经要求 C17。如果你的项目还锁在 C11建议就用 1.10 或 1.11别硬上新版否则编译期会给你一堆error: auto 说明符需要 C14之类的报错排查起来很耽误事。2.2 包管理器与 FetchContent省事但要注意版本漂移不想编译源码的话可以用系统包管理器。Ubuntu 上是sudo apt install libgtest-dev libgmock-dev注意有些老发行版里libgtest-dev装完之后还要去/usr/src/googletest手动编译一次才会生成静态库这个坑很多人都踩过——装完了却找不到libgtest.a以为装失败了其实只是没编译。我更推荐现代 CMake 的FetchContent方式它把下载 配置 编译 链接四步合成了一件事include(FetchContent) FetchContent_Declare( googletest GIT_REPOSITORY https://github.com/google/googletest.git GIT_TAG v1.14.0 ) # Windows 下需要下面这行避免覆盖父项目的编译选项 set(gtest_force_shared_crt ON CACHE BOOL FORCE) FetchContent_MakeAvailable(googletest)用FetchContent最大的好处是版本钉死在GIT_TAG上任何人 clone 你的仓库都能拉到同一份代码。这里有个必须强调的实操心得一定要写具体的 tag别写main。我见过有同学图省事写GIT_TAG main结果某天上游改了 API第二天 CI 全红人还一脸懵。用FetchContent的另一个隐藏福利是测试代码可以和生产代码共享一套编译选项和第三方依赖不用你手工维护两套配置。2.3 验证环境是否真的通了装完之后别急着写业务测试先跑一个最小例子确认链路通了#include gtest/gtest.h TEST(SmokeTest, EnvironmentWorks) { EXPECT_EQ(1 1, 2); }链接gtest_main编译运行如果输出类似下面的内容说明环境没问题[] Running 1 test from 1 test suite. [----------] Global test environment set-up. [----------] 1 test from SmokeTest [ RUN ] SmokeTest.EnvironmentWorks [ OK ] SmokeTest.EnvironmentWorks (0 ms) [----------] 1 test from SmokeTest (0 ms total) [] 1 test from 1 test suite ran. (0 ms total) [ PASSED ] 1 test.注意如果编译时报undefined reference to testing::InitGoogleTest八成是忘了链接libgtest.a如果报undefined reference to main那是忘了libgtest_main.a。这两个错误信息我见过太多次了先查链接库准没错。3. 核心骨架TEST 宏与断言体系3.1 TEST 宏展开之后到底发生了什么TEST(TestSuiteName, TestName)这个宏看起来只是个语法糖但它背后做的事其实挺多。它会自动生成一个类继承自::testing::Test并在类里定义一个TestBody()方法你写的花括号里的内容就是TestBody的实现。同时它会在全局注册表里登记这个类这样RUN_ALL_TESTS()遍历的时候就能找到它。TestSuiteName是测试套件的名字TestName是具体用例名两者拼起来就是全名TestSuiteName.TestName。这个名字很重要因为后面跑测试、过滤测试、看报告都靠它。命名我建议遵循一个约定套件名用被测的类名或模块名用例名用方法名_场景_预期结果的格式。比如TEST(StringUtil, Trim_WithLeadingSpaces_ReturnsTrimmed)这样光看名字就知道它在验证什么出问题的时候不用回去翻代码。还有一条必须刻在脑子里的规则同一个套件里用例名不能重复。gtest 检测到重复会直接报编译错误或者运行期报错不会默默覆盖。我见过有人复制粘贴忘了改名字排查半天才发现是重名。3.2 ASSERT 和 EXPECT 到底怎么选gtest 的断言分两大类区别在于失败之后是否继续往下跑ASSERT_*系列失败会直接return当前用例剩下的语句不再执行EXPECT_*系列失败只记录结果继续跑完当前用例这个区别为什么重要举个例子你写了一段从容器里取数据再计算的代码。如果容器是空的取元素这一步就已经错了后面再验证计算结果毫无意义还会输出一堆误导性的失败信息。这时候应该用ASSERT。反过来如果你要验证一个对象的五个字段其中一个字段不对不代表其他字段也不能看应该用EXPECT让自己一次看到全部问题。我个人的经验法则是前置条件、指针解引用、容器取值这类后面依赖前面的操作用ASSERT独立的语义验证一律用EXPECT。默认倾向EXPECT因为它能给你更完整的信息。还有一点ASSERT在返回void的函数里才能用如果你在辅助函数里用ASSERT但函数返回值不是void编译会直接报错这个错误提示通常不太友好看到它先想想是不是自己在返回值函数里用了ASSERT。3.3 常用断言速查表断言种类很多全背下来没必要。我把实际写代码时最常用的整理成表剩下的遇到再查文档就行。断言用途是否致命ASSERT_TRUE(cond)/EXPECT_TRUE(cond)布尔判断ASSERT 致命ASSERT_EQ(a, b)/EXPECT_EQ(a, b)相等判断ASSERT 致命ASSERT_NE/EXPECT_NE不等ASSERT 致命ASSERT_LT/LE/GT/GE大小比较ASSERT 致命EXPECT_FLOAT_EQ(a, b)浮点近似相等容忍 4 ULP 误差否EXPECT_NEAR(a, b, eps)指定误差范围自定义精度否EXPECT_STREQ(a, b)C 字符串内容比较否EXPECT_THROW(stmt, type)期望抛出指定类型异常否EXPECT_NO_THROW(stmt)期望不抛异常否EXPECT_ANY_THROW(stmt)期望抛任意异常否SUCCEED()/FAIL()强制标记成功 / 失败FAIL 致命关于浮点比较我要多说两句这是新人最容易写错的地方。EXPECT_EQ(0.1 0.2, 0.3)会失败因为浮点数在计算机里是二进制近似表示加法之后有个极小的误差。正确做法是EXPECT_NEAR(0.1 0.2, 0.3, 1e-9)明确告诉框架我允许这么大的误差。这个误差阈值怎么定看你的业务精度要求图像处理里 1e-4 可能都够了金融计算可能得 1e-12。别直接抄别人的值按你自己的量纲算。字符串同理EXPECT_EQ(str1, str2)比较的是std::string对象本身这个是可以的但如果你手上是const char*必须用EXPECT_STREQ否则比较的是两个指针地址几乎永远不相等——这个坑我见过太多人踩了测试莫名其妙失败其实只是比较了一个地址。4. 测试夹具 TEST_F把重复初始化收干净4.1 SetUp / TearDown 的调用时机你很快就会遇到一个问题好几个用例都需要构造同一个对象、加载同一份测试数据。如果每个用例里都写一遍代码又长又容易改漏。gtest 提供了夹具Fixture来解决这件事。写法是继承::testing::Test在里面重写SetUp()和TearDown()然后所有用例用TEST_F而不是TEST。#include gtest/gtest.h #include vector class StackTest : public ::testing::Test { protected: void SetUp() override { // 每个用例执行前都会跑一次 for (int i 1; i 3; i) { stack_.push(i); } } void TearDown() override { // 每个用例执行后都会跑一次即使 SetUp 抛异常也会执行 // 一般用来释放文件句柄、断开连接 } std::vectorint stack_; }; TEST_F(StackTest, SizeAfterSetupIsThree) { EXPECT_EQ(stack_.size(), 3u); } TEST_F(StackTest, TopElementIsThree) { ASSERT_FALSE(stack_.empty()); EXPECT_EQ(stack_.back(), 3); }这里有几个关键点必须讲清楚。SetUp和TearDown是每个用例各跑一次不是所有用例跑一次。这意味着用例之间天然隔离一个用例里对stack_的改动不会影响下一个用例。这也是我推荐夹具而不是全局变量的原因——全局状态是单元测试最大的敌人。如果你确实需要所有用例跑一次的共享资源比如启动一次内存数据库那应该用SetUpTestSuite()和TearDownTestSuite()老版本叫SetUpTestCase1.10 之后改名了编译报错的话先查这个。这两个是静态方法在套件里所有用例开始前和结束后各跑一次。TEST_F里能直接访问夹具的成员是因为它生成的类继承自你的夹具类成员是protected所以子类能看见。注意成员必须写成protected或者public写成private的话子类访问不到编译会报错。4.2 夹具使用中的几个坑第一个坑构造和析构 vs SetUp 和 TearDown 的选择。夹具类的构造函数和析构函数也会在每个用例前后被调用。那为什么不用构造函数因为构造函数不能安全地使用ASSERT_*断言失败时它需要return而构造函数没有返回值会导致未定义行为。SetUp()返回void可以随便用断言。所以规则是初始化逻辑里要写断言就放SetUp纯赋值、纯构造放构造函数甚至更省事。第二个坑TearDown里不要用ASSERT。因为TearDown执行时用例结果已经基本确定了此时失败信息有时候会被吞掉也不好定位。清理逻辑应该写成尽力而为的形式出错就记一条日志别指望断言能帮你定位到什么。第三个坑夹具不能复用同一个名字。每个夹具类对应一个测试套件你用TEST_F(StackTest, Xxx)和TEST_F(StackTest, Yyy)没问题但不能再定义第二个同名的夹具类。这个限制在大型项目里偶尔会造成困扰解决办法是把夹具放到不同的命名空间里。4.3 一个贴近实战的夹具例子光看vector不过瘾我拿一个真实的场景说测试一个简单的账户类每个用例都需要一个初始余额 1000 的账户。class Account { public: explicit Account(double balance) : balance_(balance) {} bool Withdraw(double amount) { if (amount 0 || amount balance_) return false; balance_ - amount; return true; } double balance() const { return balance_; } private: double balance_; }; class AccountTest : public ::testing::Test { protected: void SetUp() override { account_ std::make_uniqueAccount(1000.0); } std::unique_ptrAccount account_; }; TEST_F(AccountTest, WithdrawValidAmountSucceeds) { EXPECT_TRUE(account_-Withdraw(300.0)); EXPECT_DOUBLE_EQ(account_-balance(), 700.0); } TEST_F(AccountTest, WithdrawMoreThanBalanceFails) { EXPECT_FALSE(account_-Withdraw(1500.0)); EXPECT_DOUBLE_EQ(account_-balance(), 1000.0); } TEST_F(AccountTest, WithdrawZeroFails) { EXPECT_FALSE(account_-Withdraw(0.0)); EXPECT_DOUBLE_EQ(account_-balance(), 1000.0); }注意第二个用例取款失败之后我仍然验证余额没变。这是很多人会漏掉的一步。只验证返回值测不到失败路径有没有意外修改状态这类 bug而这类 bug 恰恰是最难在生产环境复现的。多写一行EXPECT_DOUBLE_EQ成本几乎为零收益却很大。5. 进阶玩法参数化、类型化与死亡测试5.1 值参数化 TEST_P一行代码测十组数据当你需要对同一段逻辑用多组输入验证时别复制粘贴十个用例用参数化测试。核心是三个东西继承::testing::TestWithParamT、用TEST_P写用例、用INSTANTIATE_TEST_SUITE_P注册参数集。class IsPrimeTest : public ::testing::TestWithParamstd::tupleint, bool {}; TEST_P(IsPrimeTest, MatchesExpected) { auto [input, expected] GetParam(); EXPECT_EQ(IsPrime(input), expected) 输入值为 input; } INSTANTIATE_TEST_SUITE_P( PrimeCases, IsPrimeTest, ::testing::Values( std::make_tuple(1, false), std::make_tuple(2, true), std::make_tuple(17, true), std::make_tuple(100, false), std::make_tuple(997, true) ) );有几个细节值得展开。INSTANTIATE_TEST_SUITE_P的第一个参数是实例名前缀它会拼在测试套件名前面所以最终用例全名类似PrimeCases/IsPrimeTest.MatchesExpected/0末尾的数字是参数索引。这个名字在 CI 报告里会显示前缀起得好能一眼看出这组参数的用途。版本兼容坑1.10 之前的 gtest 用的是INSTANTIATE_TEST_CASE_P1.10 之后改成TEST_SUITE系列。如果你拿着老代码在新版本上编译会看到一堆废弃警告甚至在开了-Werror的时候直接编译失败。改名字就行语义完全一致。还有一个我特别喜欢的用法EXPECT_EQ(...) 输入值为 input;。这个是给断言加自定义失败信息用的参数化测试一旦失败你不知道是第几组参数挂的加上这行能直接定位。参数化测试不加自定义信息等于给自己挖坑。参数生成的工具函数也很全Values(a, b, c)列具体值Range(start, end)生成步长为 1 的区间Range(start, end, step)带步长Bool()生成true/false两个值ValuesIn(container)从容器读。组合起来用Combine(g1, g2)可以生成笛卡尔积。5.2 类型参数化 TYPED_TEST一套逻辑测多种类型如果你写的是模板代码同一个算法要在int、float、double上跑用TYPED_TEST最合适。它和值参数化的区别在于类型参数化是把类型当参数每一组参数会编译出一份独立的代码因此可以做类型相关的事比如声明该类型的变量。template typename T class CalculatorTest : public ::testing::Test { protected: T value_ static_castT(2); }; using NumberTypes ::testing::Typesint, long, float, double; TYPED_TEST_SUITE(CalculatorTest, NumberTypes); TYPED_TEST(CalculatorTest, AdditionIsCommutative) { T a this-value_; T b static_castT(3); EXPECT_EQ(a b, b a); }在TYPED_TEST里访问夹具成员必须用this-因为模板基类的成员名在派生类里不会自动可见这是 C 模板的查找规则决定的忘了写this-会报未声明的标识符新手常常被这个卡住。还有个小细节如果你用的类型列表里有重复类型TYPED_TEST_SUITE会报错去重一下就好。5.3 死亡测试验证程序真的按预期崩了有些代码的约定就是遇到非法输入就主动终止比如防御性编程里的assert、或者某些必须 fail-fast 的初始化路径。这类行为用普通断言测不了得用死亡测试。void CrashingFunction() { int* p nullptr; *p 1; // 故意空指针解引用 } TEST(DeathTest, NullDereferenceKillsProcess) { EXPECT_DEATH(CrashingFunction(), ); }死亡测试的原理是 fork 出一个子进程去执行父进程观察子进程是否异常退出。这带来两个重要限制第一它比较慢每次都要创建进程第二它和线程有兼容性问题多线程环境下在子进程里执行代码很容易出诡异结果所以 gtest 官方建议死亡测试尽量在单线程的用例里跑或者干脆用testing::FLAGS_gtest_death_test_style threadsafe切到线程安全模式代价是更慢。死亡的匹配串支持正则但 gtest 用的是 POSIX 扩展正则不是 ECMAScript 那套写.*、[0-9]都没问题。我一般会尽量匹配关键信息而不是写空串这样能确认崩的原因对不对而不是随便崩了就算过。6. gMock 入门把烦人的依赖隔离掉6.1 为什么需要 Mock测试一个订单服务它要调支付网关、要写数据库、要发消息队列。真连这些依赖跑测试问题一大堆慢、不稳定、需要环境、失败原因难定位。而且你根本没法测支付网关超时的时候订单服务会怎样这种场景除非你能控制网关的行为。Mock 的思路是定义一个假的接口实现你想让它返回什么就返回什么想让它被调用几次就调用几次还可以断言它有没有被正确调用。这才叫真正的单元测试——只测你自己的逻辑外部世界全部可控。6.2 MOCK_METHOD 与 EXPECT_CALL先定义一个接口class PaymentGateway { public: virtual ~PaymentGateway() default; virtual bool Charge(const std::string orderId, double amount) 0; };然后用 gMock 生成假实现#include gmock/gmock.h class MockPaymentGateway : public PaymentGateway { public: MOCK_METHOD(bool, Charge, (const std::string orderId, double amount), (override)); };MOCK_METHOD的参数依次是返回类型、方法名、参数列表用括号包一层、修饰符。这里必须注意参数列表要再套一层括号这是宏解析的需要不写的话编译错误信息会比较晦涩。(override)是 C11 的说明符加上它能让编译器帮你检查签名是否匹配强烈建议加。测试里这样用TEST(OrderServiceTest, SuccessfulCharge) { MockPaymentGateway gateway; OrderService service(gateway); EXPECT_CALL(gateway, Charge(order-001, 99.9)) .Times(1) .WillOnce(::testing::Return(true)); EXPECT_TRUE(service.Pay(order-001, 99.9)); }EXPECT_CALL表达的是我期望这个调用发生如果测试跑完这个调用一次都没发生用例直接失败。这个特性非常有用它把验证行为和验证结果合到了一起。很多人只验证返回值忘了验证该调用的方法到底调了没有结果代码里少调了一次支付测试还是绿的——这种情况在重构中最容易出现。6.3 匹配器与行为设置参数不固定的时候用匹配器匹配器含义_任意值Eq(v)/Ne(v)等于 / 不等于Gt(v)/Ge(v)/Lt(v)/Le(v)大小比较IsNull()/NotNull()指针判空HasSubstr(s)字符串包含StartsWith(s)/EndsWith(s)字符串前后缀Ref(v)按引用匹配用于输出参数设置返回行为的常见写法.WillOnce(Return(x))表示调用一次返回 x多次调用就链多个WillOnce.WillRepeatedly(Return(y))表示后续所有调用都返回 y.Times(n)精确控制次数.Times(AtLeast(n))/AtMost(n)/AnyNumber()是范围形式。我给你一个实战里经常用到的组合技——模拟前两次失败、第三次成功的重试逻辑EXPECT_CALL(gateway, Charge(_, _)) .WillOnce(::testing::Return(false)) .WillOnce(::testing::Return(false)) .WillOnce(::testing::Return(true)); EXPECT_TRUE(service.PayWithRetry(order-002, 50.0, 3));这个测试能一次性验证你的重试次数够不够、重试之后有没有正确处理成功路径不用真的去等网络超时几毫秒就跑完。还有几个实用开关StrictMockT会对任何没被EXPECT_CALL声明的调用直接报错适合你想严格约束调用面的场景NiceMockT反之未声明的调用静默放行适合你只关心少数几个方法的时候。默认的Mock处于中间态——未声明的调用会给个警告但不失败。我自己的偏好是默认 Mock 就够了只在确保交互契约非常明确的时候才上 StrictMock不然重构的时候改一个调用就要修一堆测试得不偿失。注意Mock 对象必须在所有EXPECT_CALL预期都被验证完之后才能析构否则 gmock 也会报错。所以不要把 Mock 对象定义在某个提前退出的作用域里。7. 工程化落地CMake 集成与日常怎么跑7.1 CMakeLists 怎么写才算规范一个能用的测试构建脚本大概长这样cmake_minimum_required(VERSION 3.16) project(my_project CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) include(FetchContent) FetchContent_Declare( googletest GIT_REPOSITORY https://github.com/google/googletest.git GIT_TAG v1.14.0 ) set(gtest_force_shared_crt ON CACHE BOOL FORCE) FetchContent_MakeAvailable(googletest) enable_testing() add_executable(unit_tests test_account.cpp test_string_util.cpp ) target_link_libraries(unit_tests PRIVATE my_library GTest::gtest_main GTest::gmock ) include(GoogleTest) gtest_discover_tests(unit_tests)这里面的重点是用目标名而不是文件路径来链接也就是GTest::gtest_main而不是-L/xxx/libgtest_main.a。现代 CMake 的目标会自动带上头文件路径、编译选项和传递依赖写起来干净换平台也不容易出错。gtest_discover_tests是关键的一步它在构建时运行一次测试程序、把里面所有用例的名字抓出来注册成 CTest 能识别的独立测试项。这样ctest就能单独跑某一个用例CI 上也能显示具体哪个用例失败了而不是笼统的测试程序返回非零。7.2 运行期的那些命令行开关gtest 自带一批命令行参数你可能从来没用过但都挺实用参数作用--gtest_list_tests只列出所有用例不执行--gtest_filterFoo.*:*.Bar只跑匹配的用例-是排除--gtest_repeat10重复跑十遍查不稳定用例--gtest_shuffle打乱执行顺序--gtest_break_on_failure失败时触发断点方便挂调试器--gtest_outputxml:report.xml输出 XML 报告给 CI 用--gtest_also_run_disabled_tests连DISABLED_前缀的用例也跑--gtest_filter是我日常用得最多的。定位问题的时候全量跑一次要几分钟加上过滤就只跑相关的几个秒级出结果。语法是套件名.用例名*通配冒号分隔多个模式前面加-表示排除。比如--gtest_filterAccountTest.*-AccountTest.WithdrawZeroFails就是跑 AccountTest 下除了 WithdrawZeroFails 之外的全部用例。--gtest_shuffle配合--gtest_repeat是个排查隐藏依赖的神器。你写测试的时候可能不小心让用例 B 依赖了用例 A 留下的全局状态正常顺序跑永远绿一打乱就红。我第一次遇到这个问题的时候查了整整一个下午从此养成习惯新模块的测试写完先跑一次--gtest_shuffle --gtest_repeat5能提前暴露不少隐患。--gtest_outputxml:输出的是 JUnit 风格的 XML几乎所有主流 CI 工具都能直接解析用来在流水线页面上展示测试通过率和失败详情。这个格式别自己拼用框架自带的。7.3 让测试在 CI 上有意义集成了 CMake 之后CI 上的命令就是构建加ctest --output-on-failure。--output-on-failure这个参数建议永远加上否则失败的用例只能看到1 test failed看不到具体断言信息等于白跑。另外有个容易忽略的点测试失败必须让流水线失败。有些同学为了让流水线看起来是绿的把ctest的返回码忽略了比如写成ctest || true。这等于把单元测试变成了装饰品比不写还糟糕因为它会让人误以为有保护。真想让某几个已知失败的用例暂时不阻塞正确做法是加上DISABLED_前缀并附上 issue 链接说明而不是全局吞掉失败。8. 常见问题与排查实录8.1 链接类错误报错原因解决undefined reference to testing::InitGoogleTest没链接 gtest 库加GTest::gtestundefined reference to main没链gtest_main也没自己写 main加GTest::gtest_main或补 mainundefined reference to testing::internal::...链接器和编译器标准不一致统一CMAKE_CXX_STANDARDcannot find -lgtest库路径没告诉链接器用 CMake 目标名别手写-l链接错误的核心排查思路是看未定义的符号属于哪个库。testing::开头的肯定是 gtesttesting::internal::开头的有时是因为编译 gtest 和你自己代码的 C 标准不同导致符号名不匹配这个坑比较隐蔽遇到明明链接了库还说找不到符号的时候先检查两边标准是否一致。8.2 运行期的诡异现象段错误但没输出任何失败信息。这种情况多半是测试代码本身崩了不是断言失败。典型原因有访问了悬垂指针、容器越界、Mock 对象在EXPECT_CALL还没验证完就被销毁了。排查手段是先用gdb跑起来bt看栈。我提一个更省事的办法在 gtest 里加--gtest_catch_exceptions0让异常和崩溃直接暴露出来别被框架兜住。虽然默认是 0但有些环境变量会改掉它显式写一下更稳。测试通过但逻辑明显不对。八成是断言用错了。最常见的两个const char*用EXPECT_EQ比了地址浮点数直接EXPECT_EQ。还有一种更隐蔽的——EXPECT_EQ(a, b)里两个数的类型不同发生了隐式转换比如int和unsigned比较负号会变成巨大的正数。这类问题排查起来特别费劲解决办法是把类型对齐或者用EXPECT_EQ(static_castlong long(a), static_castlong long(b))显式统一。用例顺序一变就挂。前面说过用--gtest_shuffle一打乱你就知道了。根本原因通常是全局单例、静态变量、文件系统残留、网络端口占用。解决方向是让每个用例都在自己的夹具里创建所需状态退出时清理干净。真有共享的昂贵资源用SetUpTestSuite管好生命周期别让它跨套件泄漏。8.3 一套我自己常用的排查流程遇到测试问题我基本按下面这个顺序走通常两三步就能定位用--gtest_filter只跑失败的那个用例排除干扰看断言信息确认是期望值错了还是实际值错了——前者是测试写错后者是代码有问题别搞反了如果用例崩溃用--gtest_break_on_failure加gdb挂上去看栈如果断言全过但结果不对检查有没有用错断言的类型指针、浮点、跨类型比较单跑没问题、全跑有问题上--gtest_shuffle查状态污染实操心得给每个断言加 自定义信息这个习惯在只有几十个测试的时候显得啰嗦等测试涨到几百个的时候就是救命稻草。我从一开始嫌麻烦到后来变成强迫症就是因为被某个断言失败了但完全看不出是哪组数据折磨过好几次。最后再分享一个小经验。我写测试的顺序通常是先写一个一定能通过的用例把框架跑通了再去写会失败的边界用例。这样编译错误、链接错误、环境问题都在第一步暴露干净后面专心写逻辑就好。反过来先写复杂的边界用例一旦报错你分不清是环境问题还是逻辑问题排查成本翻倍。这个习惯我用了好几年每次带新人都推荐他们照做反馈都不错。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询