很多用 CodeBlocks 写 C++ 的人,第一次接触单元测试时都会有点懵。IDE 很轻量,装上 MinGW 就能编译,但测试框架不会自己出现在菜单里;去网上搜 GoogleTest 配置,出来的基本是 Visual Studio 或者纯命令行的 CMake 教程,直接照搬根本跑不起来。我也在这个坑里翻了好几个晚上的文档,后来才总结出这套办法:先把 googletest 源码编成静态库,再回到 CodeBlocks 里把头文件路径、库路径、编译器参数一条条配好。这篇文章把整套流程完整记录下来,适合刚接触单元测试的 C++ 学习者,也适合换了电脑之后需要重新搭环境的老手。1. 整体思路:CodeBlocks 和 GoogleTest 怎么搭起来1.1 为什么选择这个组合CodeBlocks 是一个不挑配置的 C++ IDE,对旧电脑尤其友好。它自带项目管理和编辑器,编译靠的是外部的 GCC 编译器,整个环境可以压缩到几百 MB。相比之下 Visual Studio 功能多,但启动速度慢,而且许可证和安装体积对很多学习场景来说都太重了。GoogleTest 是 C++ 生态里使用面很广的单元测试框架,它不依赖特定的 IDE,只要能编译 C++ 的编译器就能跑。这两个东西放在一起,等于保留了一个轻量开发环境,同时拿到了一个正规的测试框架,对于课程作业、内部工具、小规模项目维护来说非常合适。我见过不少人觉得“测试框架是 CI 服务器上的东西,本地 IDE 里配什么测试”,其实恰恰相反。本地能跑单测,开发效率提升是最明显的。改一个函数,按一次编译,立刻知道 break 了哪里,比改完代码心里发虚强太多。1.2 两种接入方式,我推荐静态库GoogleTest 接入项目有两种主流方式,我两种都试过,说下实际感受。方式一:把 googletest 源码直接加入 CodeBlocks 项目。看起来最简单,把googletest/src下的源码全部拉进项目里,头文件路径指到googletest/include,然后一起编译。问题是,GoogleTest 源码对编译器标准比较敏感,如果 CodeBlocks 默认的编译标准还是老的 C++98/C++03,会出现大量模板和内存管理的报错。虽然可以在项目属性里强行打开 C++11,但每次新建测试项目都得重复配一遍这些文件,项目目录也会被一大堆测试框架代码塞满,看起来非常乱。方式二:先把 GoogleTest 编译成静态库,测试项目里只链接库文件。也就是先单独编译出自己的libgtest.a和libgtest_main.a,然后以后每个测试项目只需要在设置里指定头文件目录和库文件目录,再加一条-lgtest链接参数,代码里直接#include <gtest/gtest.h>就行。测试项目保持干净,依赖关系也明确。我推荐方式二,原因有三个:编译一次,到处使用,不需要每个项目重新编译一遍框架源码。测试项目里的源码全是自己业务代码,看得清楚。排查问题简单,出现链接错误时,只需要检查路径和参数,不用怀疑框架源码编译环节。对比项源码直接加入预编译静态库首次配置速度快慢(需要多编译一次)后续新建测试项目每项目都要配置源码只需要指定路径项目目录整洁度被框架源码淹没干净排错难度难,容易混入框架编译错误简单推荐程度不推荐长期使用强烈推荐1.3 需要提前准备的东西在开始操作前,确认几个前提条件,缺一个后面都会卡壳:一个能正常工作的 CodeBlocks 环境,我以自带 GNU GCC Compiler 的情况为例。googletest 源码包。可以从官方仓库直接下载打包好的 release,或者用 git 拉取最新的稳定分支。一个用来存放编译产物(静态库和头文件)的目录,建议路径不要包含空格和中文,避免后面的命令行操作出现奇怪问题。基本的命令行操作能力,能进目录、执行命令,仅此而已。版本方面,我使用的是 1.10 以上的版本,文件结构和编译方式没有本质变化,下面这套操作适用于近几年的主流版本。1.4 整体步骤预览整个配置过程可以拆成三条主线:在命令行下把 googletest 源码编译成静态库。在 CodeBlocks 里建立一个测试项目,并配置好两个搜索路径和链接参数。在测试项目里写测试用例,编译运行,并学会看输出结果。我在文章里会带着每个参数解释为什么这么写,而不是只丢命令。2. 第一步:把 GoogleTest 源码编译成静态库2.1 源码文件结构先搞清楚拿到 googletest 源码后,解压出来能看到googlemock和googletest两个顶层目录。我们需要的核心内容在googletest这个目录下。真正被编译的核心编译单元,我最早以为是每个 .cc 文件都要单独编译,后来才发现官方为了方便集成,专门提供了一个汇总文件叫gtest-all.cc,它把绝大多数测试框架实现都统一包含进来了,路径在googletest/src/gtest-all.cc。所以编译的时候,只需要处理两个文件:googletest/src/gtest-all.cc:框架主体实现。googletest/src/gtest_main.cc:一个可以直接使用的main函数实现,里面调用了InitGoogleTest和RUN_ALL_TESTS。以后写测试文件的时候,你可以自己写main,也可以把gtest_main链接进去不写main。两者我都试过,建议前期你自己写一个main,这样以后如果需要在测试之前做初始化,方便修改。2.2 在命令行里编译两个对象文件打开命令行,进入 googletest 源码根目录所在的路径。以我本机目录为例子,源码解压在D:\thirdparty\googletest,我执行的命令如下:cd D:\thirdparty\googletest g++ -std=c++11 -Wall -I googletest -I googletest/include -c googletest/src/gtest-all.cc -o gtest-all.o g++ -std=c++11 -Wall -I googletest -I googletest/include -c googletest/src/gtest_main.cc -o gtest_main.o逐个解释一下参数:-std=c++11:强制指定 C++11 标准。老版本的 GCC 默认标准可能不支持 googletest 里用到的某些特性,这一步必须做。-I googletest:因为gtest-all.cc内部会按相对路径去包含其他实现文件,这个路径指向源码目录的上层,保证#include "src/gtest-internal-inl.h"这类包含能找到。-I googletest/include:让编译系统能找到gtest/gtest.h这个大门口的头文件。-c:只编译不链接,生成目标文件.o。这一步如果顺利,你会得到gtest-all.o和gtest_main.o两个文件。如果出现找不到某个头文件,检查路径是不是写错了,尤其是googletest/include这一层必须包含进去。2.3 用 ar 打包生成静态库对象文件只是中间产物,还需要打包成静态库。标准做法是用ar命令:ar rcs libgtest.a gtest-all.o ar rcs libgtest_main.a gtest_main.o参数说明:rcs表示如果没有库文件就创建,有文件就替换,并且生成索引。老手习惯直接这么用,新手不用深究。libgtest.a是主测试框架库,所有断言和测试注册逻辑都在里面。libgtest_main.a是带main的库,以后测试项目如果链接了这个库,就可以不用自己写main。打包完成后,把这两个.a文件放到一个固定目录,比如D:\thirdparty\gtest\lib,并把googletest\include目录复制到D:\thirdparty\gtest\include。以后所有测试项目统一指向D:\thirdparty\gtest\include和D:\thirdparty\gtest\lib,路径统一,管理方便。2.4 验证静态库是否真的能用这一步很容易被跳过去,但我建议花一分钟做个冒烟测试,写一个小到不能再小的测试文件:#include <gtest/gtest.h> TEST(SmokeTest, BasicAssertion) { EXPECT_EQ(1 + 1, 2); } int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }保存为smoke_test.cpp,然后命令行编译:g++ -std=c++11 -I D:\thirdparty\gtest\include smoke_test.cpp -L D:\thirdparty\gtest\lib -lgtest -lgtest_main -o smoke_test.exe注意这里的链接顺序。-lgtest_main放在-lgtest前面,因为gtest_main的实现会引用RUN_ALL_TESTS等符号,链接器处理的时候必须先把依赖方处理好。如果你把-lgtest放在前面,某些 GCC 版本会给出 undefined reference,后面排查半天也找不到原因。如果能编译出smoke_test.exe,运行后看到类似[ RUN ] SmokeTest.BasicAssertion和[ OK ]的输出,说明静态库没问题。3. 第二步:在 CodeBlocks 里建立测试项目3.1 新建一个控制台项目现在回到 CodeBlocks,开始建测试项目。选择菜单里的 New -> Project,模板选 Console Application,语言选 C++,然后按两步提示填写项目名称和保存路径。建议先建立一个空白项目目录,并且测试项目不要和你写的功能代码项目放在同一个工程里,逻辑上分离。编译器选择这里需要确认一下,项目创建对话框的编译器下拉框里选中你常用的 GNU GCC Compiler。如果列表里没有,说明全局设置里还没配置好编译器,先去 Settings -> Compiler 里把 MinGW 的安装目录指向设置好。3.2 配置头文件搜索目录和库搜索目录项目建好之后,工具栏找到 Project -> Build options,也可以直接右键项目名,在弹出的菜单里选 Build options。左侧有两个常见的配置项:Compiler settings 和 Linker settings。先在左侧选 Compiler settings,切到 Search directories 标签页,这个标签页下面是 Compiler 和 Linker 两个子页签。在 Compiler 子页签中,点击 Add 按钮,把D:\thirdparty\gtest\include加进去。这个路径就是告诉编译器,遇到#include <gtest/gtest.h>的时候,该去哪里找头文件。然后切到 Linker 子页签,同样点 Add,把D:\thirdparty\gtest\lib加进去。这个路径是告诉链接器,-lgtest里的libgtest.a在哪个目录。如果你的界面里找不到这两个子页签,也不用慌,可以直接把路径写到 Compiler settings 里的 Other options 框里。写的内容是:-I D:\thirdparty\gtest\include -L D:\thirdparty\gtest\lib这两种方式等价,本质都是给编译器和链接器传搜索路径。我更喜欢用 IDE 的 Search directories 界面,因为路径会被单独显示,看起来清楚。3.3 链接参数和 C++ 标准接下来还在同一个 Build options 窗口里,左侧切到 Linker settings。注意它下面有个 Link libraries 列表,这个列表是让你直接加.a或.lib文件的,很多新人在这里直接把整个路径加入,但这样配置方式比较脆弱,我推荐用底部的 Other linker options 文本框,手动写:-lgtest -lgtest_main顺序是有讲究的。lgtest_main在前,lgtest在后,原因我在前面冒烟测试的时候已经解释过了。如果以后你决定自己写main,不链接gtest_main,那么这里只需要写-lgtest。然后还要确认 C++ 标准。回到左侧 Compiler settings,找到 Compiler flags 或 Standard 标签页,把 C++ standard 设置为 C++11 或更高。这一步经常被忽略,但千万要做,否则编译测试文件时会出现类似stdexcept解析异常、模板展开失败之类的问题。3.4 第一版最小测试代码在项目的 main.cpp 里写入如下代码:#include <gtest/gtest.h> // 一个普通函数,后面我们用测试用例来验证它 int Add(int a, int b) { return a + b; } // 测试组名称是 AddTest,第一个测试用例叫 PositiveNumbers TEST(AddTest, PositiveNumbers) { EXPECT_EQ(Add(2, 3), 5); EXPECT_EQ(Add(10, 10), 20); } // 第二个测试用例 TEST(AddTest, NegativeNumbers) { EXPECT_EQ(Add(-1, -1), -2); } // 自己写的 main 入口 int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }这里特别说一下InitGoogleTest这行。它的作用是让框架从命令行参数中识别 GTest 自己的控制参数,比如--gtest_filter=AddTest.*这种过滤测试用例的参数。如果少了这一行,命令行参数不会被解析,过滤功能就没法用。作为日常手写 main 的习惯,我建议把这一行保留。链接参数如果加了-lgtest_main,那这个 main 就可以不写。但我不推荐依赖它,至少在你还没完全熟悉框架的时候,自己写 main 会让交后的调试更直接。4. 第三步:编译运行与结果分析4.1 编译过程中可能遇到的小问题在 CodeBlocks 里按 Ctrl+F9 编译项目,正常情况下控制台窗口会显示构建日志。如果代码没错,配置没错,你会看到类似Linking console executable...的成功提示。但第一次配置多多少少会有些状况,最常见的两种:一种是编译时报错gtest/gtest.h: No such file or directory,这说明编译器搜索路径里的 include 没配好,或者路径写错了。去 Build options 里检查一下,路径结尾是不是多了一个斜杠、少了一个斜杠,或者把/include错写成了/includes。另一种是链接时一堆undefined reference to ...,这种往往是静态库路径没配对,或者你在 Link libraries 里点选了错误的文件。回到 Search directories 的 Linker 页签检查确认。4.2 看懂测试输出编译成功后按 F9 运行。输出窗口会滚动出类似下面的内容:[==========] 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.这个输出的信息量其实很大:[ RUN ]表示接下来要运行某个测试用例。[ OK ]表示这个测试用例通过。[ FAILED ]表示断言失败,框架会标明具体哪个文件哪一行,以及两边的期望值和实际值。最后一行PASSED会统计通过数量,如果有失败,会显示FAILED的条数和明细。如果你运行后只看到空白窗口或者弹出来一个独立的终端,就看日志输出的地方,CodeBlocks 在默认设置下会把 stdout 输出到一个黑框控制台里。如果你更习惯看 IDE 内部日志,可以在运行的设置里把“独立控制台”关掉,但我不建议这么做,因为 GTest 输出会很多,IDE 内部的日志缓冲反而不好追踪。4.3 常用断言宏速记GoogleTest 有两种断言风格,EXPECT_和ASSERT_,区别在于失败后的行为:EXPECT_EQ(a, b):失败后继续运行当前测试用例。ASSERT_EQ(a, b):失败后直接终止当前测试用例。对于大多数测试场景,先用EXPECT_更稳妥,可以一次性看到多个断言失败信息。只有当前面的失败会导致后续代码出现空指针或越界等问题时,才需要用ASSERT_提前终止。下表列出我常用的断言:断言检查内容EXPECT_EQ(a, b)a 和 b 相等EXPECT_NE(a, b)a 和 b 不等EXPECT_GT(a, b)a 大于 bEXPECT_LT(a, b)a 小于 bEXPECT_STREQ(s1, s2)两个 C 风格字符串相等EXPECT_FLOAT_EQ(a, b)浮点数近似相等,带默认精度控制EXPECT_TRUE(expr)表达式为真EXPECT_FALSE(expr)表达式为假此外还有EXPECT_THROW(expr, MyException)可以测试某段代码是否抛出了指定类型的异常,这个在写健壮性测试时很常用。实际用过之后会发现,掌握这十个左右断言,日常测试需求就被全覆盖了。4.4 测试失败时的退出码运行完成后,CodeBlocks 的绿色标识可能不会告诉你测试是否通过,因为它只是看程序有没有正常退出。GoogleTest 在设计上很贴心:所有测试通过时,main返回 0;只要有测试没通过,RUN_ALL_TESTS返回非 0。这意味着如果以后你把测试接入自动构建脚本,只需要判断程序退出码即可。有的同事喜欢写一排std::cout << "all good"来判断,这是不可靠的。正确做法是看最后一行PASSED的状态。如果测试有失败,CodeBlocks 的运行窗口可能一闪而过,这时可以在控制台程序末尾加一句system("pause")之类,但这只为了方便本地看,别带到正式环境。5. 常见问题与排查实录5.1 undefined reference 错误这是最让人头疼的错误,也是我当年花了最多时间的地方。症状是编译正常通过,但链接阶段报出一长串:undefined reference to `testing::UnitTest::Run()' undefined reference to `testing::InitGoogleTest(int*, char**)'排查思路:先确认是否真的链接了静态库。在 Build options 的 Other linker options 里有没有-lgtest。然后确认库路径是否设置正确。如果libgtest.a在D:\thirdparty\gtest\lib,那-L参数必须指向这个目录,注意是包含.a文件的上一层目录,不是这一层目录。再看链接顺序。-lgtest_main -lgtest这个顺序里,lgtest_main必须在前面。如果你自己写了 main,只链接-lgtest即可,不用链接 gtest_main。有一次我套用了旧教程还在链接参数里写了-lpthread,在 Windows 的 MinGW 环境里这个问题不太需要,但在 Linux 下是必须的。如果你在 Linux 上遇到 undefined reference 到pthread_*符号,只需要在最后补一个-lpthread,位置同样是放在-lgtest之后。5.2 找不到头文件编译时出现:gtest.h: No such file or directory通常是编译器的 include 搜索路径没有正确指向include目录。要检查的地方不是你想的那个“项目目录”,而是 Build options -> Compiler settings -> Search directories -> Compiler 或 Other options 里的-I参数。如果你复制了我前面命令里的路径,注意把盘符替换成你自己的路径。还有一个容易踩的坑:手动把 googletest 源码中的include/gtest目录复制到其他位置时,目录层级可能发生变化。你需要保证最终路径是你的目录/gtest/gtest.h,因为代码里写的是<gtest/gtest.h>。如果把头文件直接铺平放,路径对不上,照样找不到。5.3 程序一运行就崩溃,但没有编译错误这种情况多半是 C++ 运行时初始化阶段的问题,常见于在 32 位和 64 位库之间发生了混搭。比如你的 CodeBlocks 编译器是 32 位,但 bochuang 的库目录里放着一个用 64 位 MinGW 编译出来的libgtest.a,链接不会报错,运行时会直接崩溃。排查办法很简单:重新用当前 CodeBlocks 对应的编译器编译一遍静态库。也可以查看编译器和库文件的架构是否一致,GCC 编译时加上-m32或-m64可以强制指定位数。我曾经同时装过 32 位和 64 位两个版本的 MinGW,有一次配置错路径导致整体编译非常离奇,最后发现就是静态库架构不匹配。5.4 C++ 标准未开启产生的疑难杂症如果忘了打开 C++11 或者更高标准,有可能链接都能通过,但运行测试时输出非常诡异,比如模板实例化失败,或者断言比较出现明显错误。实际上某些 GCC 版本在缺省标准下,std::unique_ptr这类特性的行为并不一致。我的建议是直接把编译标准设成 C++17,因为新版本 googletest 已经大量用到 C++14 的特性,老默认标准容易出问题。在 Build options 的 Compiler settings 里找到 C++ standard 下拉菜单,选 C++17,省心。5.5 路径带空格或中文目录名CodeBlocks 项目路径如果出现在带空格的目录,比如C:\My Projects\TestProject,某些源码内部会做字符串拼接,就可能出现引号缺失导致找不到文件的问题。更麻烦的是中文目录,某些旧版 MinGW 对中文路径的编码支持并不好。最稳妥的办法是把测试代码和依赖库全部放在纯英文、无空格的路径下。这一点在开头准备阶段就要注意,不要等出了问题再改。5.6 要不要单独建一个测试工程文件我的做法是:主功能项目一个工程,测试项目单独一个工程。原因很简单,测试工程需要链接 GoogleTest 静态库,加上过滤器和输出参数,构建配置和主项目差异很大。如果把测试直接丢进主项目,主项目每次编译都要跑一遍全套测试,而且一加测试代码,主项目的可执行文件变成测试驱动,严重影响调试流程。很多同事会问为什么不在主项目里右键添加一个 main 文件,看这里:CodeBlocks 的 Console Application 只有一个入口,你加了自己的 main,原有的 main 就会冲突。拆成两个项目,主项目调什么函数,测试项目链接主项目的源代码或静态库,这种结构想复杂都难。6. 我后面一直沿用的实践习惯6.1 把所有外部依赖固定在同一个目录结构下我在第 2 节提到的目录结构,实际用了很久:D:\thirdparty\ gtest\ include\ gtest\ lib\ libgtest.a libgtest_main.a所有项目都统一指向这里。这样一来,每换一台机器,只要把这个目录拷过去,重新配置一下 CodeBlocks 里的路径,测试项目立刻就能跑。不要每个项目单独复制一份 googletest 源码,那样升级的时候就要改 N 个项目。6.2 使用 TestFixture 组织重复的测试数据平时测简单函数可以直接用TEST宏,但当测试场景里需要构造相同的对象、相同的初始状态,重复代码就会爆炸。GoogleTest 提供了TestFixture,基本玩法是把公共的初始化数据放在继承testing::Test的子类里,然后用TEST_F宏写用例。代码看起来大概是:class StackTest : public testing::Test { protected: void SetUp() override { stack_.push(1); } MyStack stack_; }; TEST_F(StackTest, PopAfterPush) { EXPECT_EQ(stack_.pop(), 1); }这里SetUp会在每个测试用例执行前被调用,避免每个测试里都重复写建栈逻辑。在配置完 GoogleTest 之后,下一个值得花时间学的就是它。6.3 测试命名尽量带上业务模块前缀我已经吃够了一个项目一大堆测试部分散乱的亏。负责任的做法是使用结构化的命名,像OrderModuleTest.CheckDiscount、UserModuleTest.LoginFailure,这样测试失败时,一看名字就知道属于哪个模块。GTest 的--gtest_filter参数还可以按模块前缀过滤,连续跑测试的时候特别有用,比如:--gtest_filter=UserModuleTest.*这个功能在 IDE 里同样可以传参,方法是在 Project -> Set programs arguments 里把过滤规则填进去,运行测试时只关心自己模块的用例,效率高不少。6.4 顺手引入一个简单的回归命令脚本CodeBlocks 里鼠标点运行很方便,但真正让我体会到测试价值的是在命令行下做一个快速回归。写完一批测试,我通常在测试项目目录下开一个终端,执行:./测试目标可执行文件名.exe --gtest_color=no加上--gtest_color=no是为了避免终端不支持颜色时看到的乱码。然后把输出重定向到日志文件里,直接用文本对比确认是否有新的 fail 关键字。这种流水线式检查对改动频繁的模块帮助很大,而且不需要打开 IDE,编译和测试都是自动化的。我一直觉得,单元测试框架的配置只是折腾过程中的一小步,真正有价值的是把“跑了没”变成“跑了对不对”。本地 IDE 配上 GoogleTest 之后,写代码的安全感会明显不一样,尤其是重构代码时,改完一编译、一测试,几分钟就能确认没有把老功能弄坏。配置过程不难,但第一次总会踩些莫名其妙的坑,希望这篇记录能给你省下我当初浪费的晚上。
很多用 CodeBlocks 写 C++ 的人,第一次接触单元测试时都会有点懵。IDE 很轻量,装上 MinGW 就能编译,但测试框架不会自己出现在菜单里;去网上搜 GoogleTest 配置,出来的基本是 Visual Studio 或者纯命令行的 CMake 教程,直接照搬根本跑不起来。我也在这个坑里翻了好几个晚上的文档,后来才总结出这套办法:先把 googletest 源码编成静态库,再回到 CodeBlocks 里把头文件路径、库路径、编译器参数一条条配好。这篇文章把整套流程完整记录下来,适合刚接触单元测试的 C++ 学习者,也适合换了电脑之后需要重新搭环境的老手。1. 整体思路:CodeBlocks 和 GoogleTest 怎么搭起来1.1 为什么选择这个组合CodeBlocks 是一个不挑配置的 C++ IDE,对旧电脑尤其友好。它自带项目管理和编辑器,编译靠的是外部的 GCC 编译器,整个环境可以压缩到几百 MB。相比之下 Visual Studio 功能多,但启动速度慢,而且许可证和安装体积对很多学习场景来说都太重了。GoogleTest 是 C++ 生态里使用面很广的单元测试框架,它不依赖特定的 IDE,只要能编译 C++ 的编译器就能跑。这两个东西放在一起,等于保留了一个轻量开发环境,同时拿到了一个正规的测试框架,对于课程作业、内部工具、小规模项目维护来说非常合适。我见过不少人觉得“测试框架是 CI 服务器上的东西,本地 IDE 里配什么测试”,其实恰恰相反。本地能跑单测,开发效率提升是最明显的。改一个函数,按一次编译,立刻知道 break 了哪里,比改完代码心里发虚强太多。1.2 两种接入方式,我推荐静态库GoogleTest 接入项目有两种主流方式,我两种都试过,说下实际感受。方式一:把 googletest 源码直接加入 CodeBlocks 项目。看起来最简单,把googletest/src下的源码全部拉进项目里,头文件路径指到googletest/include,然后一起编译。问题是,GoogleTest 源码对编译器标准比较敏感,如果 CodeBlocks 默认的编译标准还是老的 C++98/C++03,会出现大量模板和内存管理的报错。虽然可以在项目属性里强行打开 C++11,但每次新建测试项目都得重复配一遍这些文件,项目目录也会被一大堆测试框架代码塞满,看起来非常乱。方式二:先把 GoogleTest 编译成静态库,测试项目里只链接库文件。也就是先单独编译出自己的libgtest.a和libgtest_main.a,然后以后每个测试项目只需要在设置里指定头文件目录和库文件目录,再加一条-lgtest链接参数,代码里直接#include <gtest/gtest.h>就行。测试项目保持干净,依赖关系也明确。我推荐方式二,原因有三个:编译一次,到处使用,不需要每个项目重新编译一遍框架源码。测试项目里的源码全是自己业务代码,看得清楚。排查问题简单,出现链接错误时,只需要检查路径和参数,不用怀疑框架源码编译环节。对比项源码直接加入预编译静态库首次配置速度快慢(需要多编译一次)后续新建测试项目每项目都要配置源码只需要指定路径项目目录整洁度被框架源码淹没干净排错难度难,容易混入框架编译错误简单推荐程度不推荐长期使用强烈推荐1.3 需要提前准备的东西在开始操作前,确认几个前提条件,缺一个后面都会卡壳:一个能正常工作的 CodeBlocks 环境,我以自带 GNU GCC Compiler 的情况为例。googletest 源码包。可以从官方仓库直接下载打包好的 release,或者用 git 拉取最新的稳定分支。一个用来存放编译产物(静态库和头文件)的目录,建议路径不要包含空格和中文,避免后面的命令行操作出现奇怪问题。基本的命令行操作能力,能进目录、执行命令,仅此而已。版本方面,我使用的是 1.10 以上的版本,文件结构和编译方式没有本质变化,下面这套操作适用于近几年的主流版本。1.4 整体步骤预览整个配置过程可以拆成三条主线:在命令行下把 googletest 源码编译成静态库。在 CodeBlocks 里建立一个测试项目,并配置好两个搜索路径和链接参数。在测试项目里写测试用例,编译运行,并学会看输出结果。我在文章里会带着每个参数解释为什么这么写,而不是只丢命令。2. 第一步:把 GoogleTest 源码编译成静态库2.1 源码文件结构先搞清楚拿到 googletest 源码后,解压出来能看到googlemock和googletest两个顶层目录。我们需要的核心内容在googletest这个目录下。真正被编译的核心编译单元,我最早以为是每个 .cc 文件都要单独编译,后来才发现官方为了方便集成,专门提供了一个汇总文件叫gtest-all.cc,它把绝大多数测试框架实现都统一包含进来了,路径在googletest/src/gtest-all.cc。所以编译的时候,只需要处理两个文件:googletest/src/gtest-all.cc:框架主体实现。googletest/src/gtest_main.cc:一个可以直接使用的main函数实现,里面调用了InitGoogleTest和RUN_ALL_TESTS。以后写测试文件的时候,你可以自己写main,也可以把gtest_main链接进去不写main。两者我都试过,建议前期你自己写一个main,这样以后如果需要在测试之前做初始化,方便修改。2.2 在命令行里编译两个对象文件打开命令行,进入 googletest 源码根目录所在的路径。以我本机目录为例子,源码解压在D:\thirdparty\googletest,我执行的命令如下:cd D:\thirdparty\googletest g++ -std=c++11 -Wall -I googletest -I googletest/include -c googletest/src/gtest-all.cc -o gtest-all.o g++ -std=c++11 -Wall -I googletest -I googletest/include -c googletest/src/gtest_main.cc -o gtest_main.o逐个解释一下参数:-std=c++11:强制指定 C++11 标准。老版本的 GCC 默认标准可能不支持 googletest 里用到的某些特性,这一步必须做。-I googletest:因为gtest-all.cc内部会按相对路径去包含其他实现文件,这个路径指向源码目录的上层,保证#include "src/gtest-internal-inl.h"这类包含能找到。-I googletest/include:让编译系统能找到gtest/gtest.h这个大门口的头文件。-c:只编译不链接,生成目标文件.o。这一步如果顺利,你会得到gtest-all.o和gtest_main.o两个文件。如果出现找不到某个头文件,检查路径是不是写错了,尤其是googletest/include这一层必须包含进去。2.3 用 ar 打包生成静态库对象文件只是中间产物,还需要打包成静态库。标准做法是用ar命令:ar rcs libgtest.a gtest-all.o ar rcs libgtest_main.a gtest_main.o参数说明:rcs表示如果没有库文件就创建,有文件就替换,并且生成索引。老手习惯直接这么用,新手不用深究。libgtest.a是主测试框架库,所有断言和测试注册逻辑都在里面。libgtest_main.a是带main的库,以后测试项目如果链接了这个库,就可以不用自己写main。打包完成后,把这两个.a文件放到一个固定目录,比如D:\thirdparty\gtest\lib,并把googletest\include目录复制到D:\thirdparty\gtest\include。以后所有测试项目统一指向D:\thirdparty\gtest\include和D:\thirdparty\gtest\lib,路径统一,管理方便。2.4 验证静态库是否真的能用这一步很容易被跳过去,但我建议花一分钟做个冒烟测试,写一个小到不能再小的测试文件:#include <gtest/gtest.h> TEST(SmokeTest, BasicAssertion) { EXPECT_EQ(1 + 1, 2); } int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }保存为smoke_test.cpp,然后命令行编译:g++ -std=c++11 -I D:\thirdparty\gtest\include smoke_test.cpp -L D:\thirdparty\gtest\lib -lgtest -lgtest_main -o smoke_test.exe注意这里的链接顺序。-lgtest_main放在-lgtest前面,因为gtest_main的实现会引用RUN_ALL_TESTS等符号,链接器处理的时候必须先把依赖方处理好。如果你把-lgtest放在前面,某些 GCC 版本会给出 undefined reference,后面排查半天也找不到原因。如果能编译出smoke_test.exe,运行后看到类似[ RUN ] SmokeTest.BasicAssertion和[ OK ]的输出,说明静态库没问题。3. 第二步:在 CodeBlocks 里建立测试项目3.1 新建一个控制台项目现在回到 CodeBlocks,开始建测试项目。选择菜单里的 New -> Project,模板选 Console Application,语言选 C++,然后按两步提示填写项目名称和保存路径。建议先建立一个空白项目目录,并且测试项目不要和你写的功能代码项目放在同一个工程里,逻辑上分离。编译器选择这里需要确认一下,项目创建对话框的编译器下拉框里选中你常用的 GNU GCC Compiler。如果列表里没有,说明全局设置里还没配置好编译器,先去 Settings -> Compiler 里把 MinGW 的安装目录指向设置好。3.2 配置头文件搜索目录和库搜索目录项目建好之后,工具栏找到 Project -> Build options,也可以直接右键项目名,在弹出的菜单里选 Build options。左侧有两个常见的配置项:Compiler settings 和 Linker settings。先在左侧选 Compiler settings,切到 Search directories 标签页,这个标签页下面是 Compiler 和 Linker 两个子页签。在 Compiler 子页签中,点击 Add 按钮,把D:\thirdparty\gtest\include加进去。这个路径就是告诉编译器,遇到#include <gtest/gtest.h>的时候,该去哪里找头文件。然后切到 Linker 子页签,同样点 Add,把D:\thirdparty\gtest\lib加进去。这个路径是告诉链接器,-lgtest里的libgtest.a在哪个目录。如果你的界面里找不到这两个子页签,也不用慌,可以直接把路径写到 Compiler settings 里的 Other options 框里。写的内容是:-I D:\thirdparty\gtest\include -L D:\thirdparty\gtest\lib这两种方式等价,本质都是给编译器和链接器传搜索路径。我更喜欢用 IDE 的 Search directories 界面,因为路径会被单独显示,看起来清楚。3.3 链接参数和 C++ 标准接下来还在同一个 Build options 窗口里,左侧切到 Linker settings。注意它下面有个 Link libraries 列表,这个列表是让你直接加.a或.lib文件的,很多新人在这里直接把整个路径加入,但这样配置方式比较脆弱,我推荐用底部的 Other linker options 文本框,手动写:-lgtest -lgtest_main顺序是有讲究的。lgtest_main在前,lgtest在后,原因我在前面冒烟测试的时候已经解释过了。如果以后你决定自己写main,不链接gtest_main,那么这里只需要写-lgtest。然后还要确认 C++ 标准。回到左侧 Compiler settings,找到 Compiler flags 或 Standard 标签页,把 C++ standard 设置为 C++11 或更高。这一步经常被忽略,但千万要做,否则编译测试文件时会出现类似stdexcept解析异常、模板展开失败之类的问题。3.4 第一版最小测试代码在项目的 main.cpp 里写入如下代码:#include <gtest/gtest.h> // 一个普通函数,后面我们用测试用例来验证它 int Add(int a, int b) { return a + b; } // 测试组名称是 AddTest,第一个测试用例叫 PositiveNumbers TEST(AddTest, PositiveNumbers) { EXPECT_EQ(Add(2, 3), 5); EXPECT_EQ(Add(10, 10), 20); } // 第二个测试用例 TEST(AddTest, NegativeNumbers) { EXPECT_EQ(Add(-1, -1), -2); } // 自己写的 main 入口 int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }这里特别说一下InitGoogleTest这行。它的作用是让框架从命令行参数中识别 GTest 自己的控制参数,比如--gtest_filter=AddTest.*这种过滤测试用例的参数。如果少了这一行,命令行参数不会被解析,过滤功能就没法用。作为日常手写 main 的习惯,我建议把这一行保留。链接参数如果加了-lgtest_main,那这个 main 就可以不写。但我不推荐依赖它,至少在你还没完全熟悉框架的时候,自己写 main 会让交后的调试更直接。4. 第三步:编译运行与结果分析4.1 编译过程中可能遇到的小问题在 CodeBlocks 里按 Ctrl+F9 编译项目,正常情况下控制台窗口会显示构建日志。如果代码没错,配置没错,你会看到类似Linking console executable...的成功提示。但第一次配置多多少少会有些状况,最常见的两种:一种是编译时报错gtest/gtest.h: No such file or directory,这说明编译器搜索路径里的 include 没配好,或者路径写错了。去 Build options 里检查一下,路径结尾是不是多了一个斜杠、少了一个斜杠,或者把/include错写成了/includes。另一种是链接时一堆undefined reference to ...,这种往往是静态库路径没配对,或者你在 Link libraries 里点选了错误的文件。回到 Search directories 的 Linker 页签检查确认。4.2 看懂测试输出编译成功后按 F9 运行。输出窗口会滚动出类似下面的内容:[==========] 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.这个输出的信息量其实很大:[ RUN ]表示接下来要运行某个测试用例。[ OK ]表示这个测试用例通过。[ FAILED ]表示断言失败,框架会标明具体哪个文件哪一行,以及两边的期望值和实际值。最后一行PASSED会统计通过数量,如果有失败,会显示FAILED的条数和明细。如果你运行后只看到空白窗口或者弹出来一个独立的终端,就看日志输出的地方,CodeBlocks 在默认设置下会把 stdout 输出到一个黑框控制台里。如果你更习惯看 IDE 内部日志,可以在运行的设置里把“独立控制台”关掉,但我不建议这么做,因为 GTest 输出会很多,IDE 内部的日志缓冲反而不好追踪。4.3 常用断言宏速记GoogleTest 有两种断言风格,EXPECT_和ASSERT_,区别在于失败后的行为:EXPECT_EQ(a, b):失败后继续运行当前测试用例。ASSERT_EQ(a, b):失败后直接终止当前测试用例。对于大多数测试场景,先用EXPECT_更稳妥,可以一次性看到多个断言失败信息。只有当前面的失败会导致后续代码出现空指针或越界等问题时,才需要用ASSERT_提前终止。下表列出我常用的断言:断言检查内容EXPECT_EQ(a, b)a 和 b 相等EXPECT_NE(a, b)a 和 b 不等EXPECT_GT(a, b)a 大于 bEXPECT_LT(a, b)a 小于 bEXPECT_STREQ(s1, s2)两个 C 风格字符串相等EXPECT_FLOAT_EQ(a, b)浮点数近似相等,带默认精度控制EXPECT_TRUE(expr)表达式为真EXPECT_FALSE(expr)表达式为假此外还有EXPECT_THROW(expr, MyException)可以测试某段代码是否抛出了指定类型的异常,这个在写健壮性测试时很常用。实际用过之后会发现,掌握这十个左右断言,日常测试需求就被全覆盖了。4.4 测试失败时的退出码运行完成后,CodeBlocks 的绿色标识可能不会告诉你测试是否通过,因为它只是看程序有没有正常退出。GoogleTest 在设计上很贴心:所有测试通过时,main返回 0;只要有测试没通过,RUN_ALL_TESTS返回非 0。这意味着如果以后你把测试接入自动构建脚本,只需要判断程序退出码即可。有的同事喜欢写一排std::cout << "all good"来判断,这是不可靠的。正确做法是看最后一行PASSED的状态。如果测试有失败,CodeBlocks 的运行窗口可能一闪而过,这时可以在控制台程序末尾加一句system("pause")之类,但这只为了方便本地看,别带到正式环境。5. 常见问题与排查实录5.1 undefined reference 错误这是最让人头疼的错误,也是我当年花了最多时间的地方。症状是编译正常通过,但链接阶段报出一长串:undefined reference to `testing::UnitTest::Run()' undefined reference to `testing::InitGoogleTest(int*, char**)'排查思路:先确认是否真的链接了静态库。在 Build options 的 Other linker options 里有没有-lgtest。然后确认库路径是否设置正确。如果libgtest.a在D:\thirdparty\gtest\lib,那-L参数必须指向这个目录,注意是包含.a文件的上一层目录,不是这一层目录。再看链接顺序。-lgtest_main -lgtest这个顺序里,lgtest_main必须在前面。如果你自己写了 main,只链接-lgtest即可,不用链接 gtest_main。有一次我套用了旧教程还在链接参数里写了-lpthread,在 Windows 的 MinGW 环境里这个问题不太需要,但在 Linux 下是必须的。如果你在 Linux 上遇到 undefined reference 到pthread_*符号,只需要在最后补一个-lpthread,位置同样是放在-lgtest之后。5.2 找不到头文件编译时出现:gtest.h: No such file or directory通常是编译器的 include 搜索路径没有正确指向include目录。要检查的地方不是你想的那个“项目目录”,而是 Build options -> Compiler settings -> Search directories -> Compiler 或 Other options 里的-I参数。如果你复制了我前面命令里的路径,注意把盘符替换成你自己的路径。还有一个容易踩的坑:手动把 googletest 源码中的include/gtest目录复制到其他位置时,目录层级可能发生变化。你需要保证最终路径是你的目录/gtest/gtest.h,因为代码里写的是<gtest/gtest.h>。如果把头文件直接铺平放,路径对不上,照样找不到。5.3 程序一运行就崩溃,但没有编译错误这种情况多半是 C++ 运行时初始化阶段的问题,常见于在 32 位和 64 位库之间发生了混搭。比如你的 CodeBlocks 编译器是 32 位,但 bochuang 的库目录里放着一个用 64 位 MinGW 编译出来的libgtest.a,链接不会报错,运行时会直接崩溃。排查办法很简单:重新用当前 CodeBlocks 对应的编译器编译一遍静态库。也可以查看编译器和库文件的架构是否一致,GCC 编译时加上-m32或-m64可以强制指定位数。我曾经同时装过 32 位和 64 位两个版本的 MinGW,有一次配置错路径导致整体编译非常离奇,最后发现就是静态库架构不匹配。5.4 C++ 标准未开启产生的疑难杂症如果忘了打开 C++11 或者更高标准,有可能链接都能通过,但运行测试时输出非常诡异,比如模板实例化失败,或者断言比较出现明显错误。实际上某些 GCC 版本在缺省标准下,std::unique_ptr这类特性的行为并不一致。我的建议是直接把编译标准设成 C++17,因为新版本 googletest 已经大量用到 C++14 的特性,老默认标准容易出问题。在 Build options 的 Compiler settings 里找到 C++ standard 下拉菜单,选 C++17,省心。5.5 路径带空格或中文目录名CodeBlocks 项目路径如果出现在带空格的目录,比如C:\My Projects\TestProject,某些源码内部会做字符串拼接,就可能出现引号缺失导致找不到文件的问题。更麻烦的是中文目录,某些旧版 MinGW 对中文路径的编码支持并不好。最稳妥的办法是把测试代码和依赖库全部放在纯英文、无空格的路径下。这一点在开头准备阶段就要注意,不要等出了问题再改。5.6 要不要单独建一个测试工程文件我的做法是:主功能项目一个工程,测试项目单独一个工程。原因很简单,测试工程需要链接 GoogleTest 静态库,加上过滤器和输出参数,构建配置和主项目差异很大。如果把测试直接丢进主项目,主项目每次编译都要跑一遍全套测试,而且一加测试代码,主项目的可执行文件变成测试驱动,严重影响调试流程。很多同事会问为什么不在主项目里右键添加一个 main 文件,看这里:CodeBlocks 的 Console Application 只有一个入口,你加了自己的 main,原有的 main 就会冲突。拆成两个项目,主项目调什么函数,测试项目链接主项目的源代码或静态库,这种结构想复杂都难。6. 我后面一直沿用的实践习惯6.1 把所有外部依赖固定在同一个目录结构下我在第 2 节提到的目录结构,实际用了很久:D:\thirdparty\ gtest\ include\ gtest\ lib\ libgtest.a libgtest_main.a所有项目都统一指向这里。这样一来,每换一台机器,只要把这个目录拷过去,重新配置一下 CodeBlocks 里的路径,测试项目立刻就能跑。不要每个项目单独复制一份 googletest 源码,那样升级的时候就要改 N 个项目。6.2 使用 TestFixture 组织重复的测试数据平时测简单函数可以直接用TEST宏,但当测试场景里需要构造相同的对象、相同的初始状态,重复代码就会爆炸。GoogleTest 提供了TestFixture,基本玩法是把公共的初始化数据放在继承testing::Test的子类里,然后用TEST_F宏写用例。代码看起来大概是:class StackTest : public testing::Test { protected: void SetUp() override { stack_.push(1); } MyStack stack_; }; TEST_F(StackTest, PopAfterPush) { EXPECT_EQ(stack_.pop(), 1); }这里SetUp会在每个测试用例执行前被调用,避免每个测试里都重复写建栈逻辑。在配置完 GoogleTest 之后,下一个值得花时间学的就是它。6.3 测试命名尽量带上业务模块前缀我已经吃够了一个项目一大堆测试部分散乱的亏。负责任的做法是使用结构化的命名,像OrderModuleTest.CheckDiscount、UserModuleTest.LoginFailure,这样测试失败时,一看名字就知道属于哪个模块。GTest 的--gtest_filter参数还可以按模块前缀过滤,连续跑测试的时候特别有用,比如:--gtest_filter=UserModuleTest.*这个功能在 IDE 里同样可以传参,方法是在 Project -> Set programs arguments 里把过滤规则填进去,运行测试时只关心自己模块的用例,效率高不少。6.4 顺手引入一个简单的回归命令脚本CodeBlocks 里鼠标点运行很方便,但真正让我体会到测试价值的是在命令行下做一个快速回归。写完一批测试,我通常在测试项目目录下开一个终端,执行:./测试目标可执行文件名.exe --gtest_color=no加上--gtest_color=no是为了避免终端不支持颜色时看到的乱码。然后把输出重定向到日志文件里,直接用文本对比确认是否有新的 fail 关键字。这种流水线式检查对改动频繁的模块帮助很大,而且不需要打开 IDE,编译和测试都是自动化的。我一直觉得,单元测试框架的配置只是折腾过程中的一小步,真正有价值的是把“跑了没”变成“跑了对不对”。本地 IDE 配上 GoogleTest 之后,写代码的安全感会明显不一样,尤其是重构代码时,改完一编译、一测试,几分钟就能确认没有把老功能弄坏。配置过程不难,但第一次总会踩些莫名其妙的坑,希望这篇记录能给你省下我当初浪费的晚上。