CodeBlocks配置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 默认的编译标准还是老的 C98/C03会出现大量模板和内存管理的报错。虽然可以在项目属性里强行打开 C11但每次新建测试项目都得重复配一遍这些文件项目目录也会被一大堆测试框架代码塞满看起来非常乱。方式二先把 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 -stdc11 -Wall -I googletest -I googletest/include -c googletest/src/gtest-all.cc -o gtest-all.o g -stdc11 -Wall -I googletest -I googletest/include -c googletest/src/gtest_main.cc -o gtest_main.o逐个解释一下参数-stdc11强制指定 C11 标准。老版本的 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 -stdc11 -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 设置为 C11 或更高。这一步经常被忽略但千万要做否则编译测试文件时会出现类似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_filterAddTest.*这种过滤测试用例的参数。如果少了这一行命令行参数不会被解析过滤功能就没法用。作为日常手写 main 的习惯我建议把这一行保留。链接参数如果加了-lgtest_main那这个 main 就可以不写。但我不推荐依赖它至少在你还没完全熟悉框架的时候自己写 main 会让交后的调试更直接。4. 第三步编译运行与结果分析4.1 编译过程中可能遇到的小问题在 CodeBlocks 里按 CtrlF9 编译项目正常情况下控制台窗口会显示构建日志。如果代码没错配置没错你会看到类似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 标准未开启产生的疑难杂症如果忘了打开 C11 或者更高标准有可能链接都能通过但运行测试时输出非常诡异比如模板实例化失败或者断言比较出现明显错误。实际上某些 GCC 版本在缺省标准下std::unique_ptr这类特性的行为并不一致。我的建议是直接把编译标准设成 C17因为新版本 googletest 已经大量用到 C14 的特性老默认标准容易出问题。在 Build options 的 Compiler settings 里找到 C standard 下拉菜单选 C17省心。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_filterUserModuleTest.*这个功能在 IDE 里同样可以传参方法是在 Project - Set programs arguments 里把过滤规则填进去运行测试时只关心自己模块的用例效率高不少。6.4 顺手引入一个简单的回归命令脚本CodeBlocks 里鼠标点运行很方便但真正让我体会到测试价值的是在命令行下做一个快速回归。写完一批测试我通常在测试项目目录下开一个终端执行./测试目标可执行文件名.exe --gtest_colorno加上--gtest_colorno是为了避免终端不支持颜色时看到的乱码。然后把输出重定向到日志文件里直接用文本对比确认是否有新的 fail 关键字。这种流水线式检查对改动频繁的模块帮助很大而且不需要打开 IDE编译和测试都是自动化的。我一直觉得单元测试框架的配置只是折腾过程中的一小步真正有价值的是把“跑了没”变成“跑了对不对”。本地 IDE 配上 GoogleTest 之后写代码的安全感会明显不一样尤其是重构代码时改完一编译、一测试几分钟就能确认没有把老功能弄坏。配置过程不难但第一次总会踩些莫名其妙的坑希望这篇记录能给你省下我当初浪费的晚上。