C++ TDD 入门:从搭建 GTest 框架到第一个示例
你想要入门 C++ 语言的 TDD 实践,核心是先搭建测试框架环境,再遵循「红-绿-重构」的循环落地最小功能单元。下面我会以Google Test(GTest,C++ 最主流的 TDD 测试框架)为例,提供从「环境搭建→第一个 TDD 示例→完整循环落地」的分步指南,适配 Linux(CentOS/Ubuntu)环境,新手也能快速上手。
一、前置准备
环境:Linux 系统(CentOS 8/9 或 Ubuntu 18.04+),已安装
g++(C++ 编译器)、cmake(构建工具)、git(拉取 GTest 源码)。验证编译器是否安装:
g++ --version cmake --version若未安装,执行以下命令补装:
# CentOS/RHEL dnf install -y gcc gcc-c++ cmake git # Ubuntu/Debian apt update && apt install -y g++ cmake git
二、步骤1:搭建 Google Test(GTest)环境
GTest 是 C++ 最成熟、最常用的单元测试框架,支持 TDD 所需的自动化测试、断言判断、测试用例隔离等核心功能,我们采用「源码编译安装」(确保版本最新,适配性更好)。
1.1 拉取 GTest 源码
# 创建工作目录,后续所有操作均在该目录下进行 mkdir -p ~/cpp_tdd_demo && cd ~/cpp_tdd_demo # 拉取 GTest 源码(官方镜像,稳定版) git clone https://github.com/google/googletest.git1.2 编译并安装 GTest
GTest 采用 CMake 构建,执行以下命令编译安装(安装到系统全局,后续所有项目均可直接引用):
# 进入 googletest 目录,创建构建目录 cd googletest && mkdir build && cd build # 生成 Makefile(指定安装路径为 /usr/local,系统全局可访问) cmake .. -DCMAKE_INSTALL_PREFIX=/usr/local # 编译(-j 后接CPU核心数,加快编译速度,如 -j4) make -j4 # 安装到系统(root 权限,需输入密码) sudo make install1.3 验证 GTest 安装是否成功
安装完成后,系统会在/usr/local/include/生成gtest/头文件目录,在/usr/local/lib64/(CentOS)或/usr/local/lib/(Ubuntu)生成库文件,验证命令:
# 查看头文件是否存在 ls /usr/local/include/gtest # 查看库文件是否存在 ls /usr/local/lib64 | grep gtest # CentOS # ls /usr/local/lib | grep gtest # Ubuntu若能看到gtest.h头文件和libgtest.a、libgtest_main.a库文件,说明安装成功。
三、步骤2:C++ TDD 入门示例(实现一个简单的加法函数)
我们以「实现一个整数加法函数int Add(int a, int b)」为例,完整遵循 TDD 「红-绿-重构」循环,这是最适合新手的入门案例。
项目结构说明
先创建清晰的项目结构(TDD 项目需分离「测试代码」和「业务代码」,便于维护):
# 回到 cpp_tdd_demo 目录,创建项目结构 cd ~/cpp_tdd_demo mkdir -p add_demo/{src,test} cd add_demosrc/:存放业务代码(.h 头文件、.cpp 实现文件);test/:存放 GTest 测试用例代码;后续会在
add_demo/下创建CMakeLists.txt,用于构建整个项目。
循环1:红(Red)- 编写失败的测试用例
核心:先写测试用例,明确功能预期,此时无业务代码,测试必然失败。
2.1 编写测试用例代码
进入test/目录,创建测试文件test_add.cpp:
vim test/test_add.cpp粘贴以下代码(GTest 测试用例的标准写法):
// 引入 GTest 核心头文件 #include <gtest/gtest.h> // 引入业务代码头文件(此时还未创建,先声明函数,保证编译不报错) // 后续会创建 src/add.h,这里先提前声明 int Add(int a, int b); // 定义测试套件(Test Suite):命名为 AddFunctionTest,对应要测试的模块 TEST(AddFunctionTest, NormalInputTest) { // 断言(Assert):判断 Add(1, 2) 的返回值是否等于 3 // EXPECT_EQ:预期相等,若不相等则测试失败(不会终止整个测试进程) // GTest 还有 ASSERT_EQ(断言失败则终止当前测试用例),新手优先用 EXPECT_* EXPECT_EQ(Add(1, 2), 3); EXPECT_EQ(Add(0, 0), 0); EXPECT_EQ(Add(-1, 1), 0); EXPECT_EQ(Add(100, 200), 300); } // 测试套件的第二个测试用例:测试边界值(TDD 要求覆盖边界条件) TEST(AddFunctionTest, BoundaryInputTest) { EXPECT_EQ(Add(INT_MAX, 0), INT_MAX); // 最大整数 + 0 EXPECT_EQ(Add(INT_MIN, 0), INT_MIN); // 最小整数 + 0 } // GTest 主函数:用于运行所有测试用例(可直接复用,无需修改) // 也可以不写该主函数,编译时链接 libgtest_main.a 即可 int main(int argc, char **argv) { // 初始化 GTest testing::InitGoogleTest(&argc, argv); // 运行所有测试用例 return RUN_ALL_TESTS(); }保存退出(Esc→:wq)。
2.2 编写 CMake 构建文件
回到add_demo/目录,创建CMakeLists.txt,用于构建测试项目:
vim CMakeLists.txt粘贴以下内容:
# 要求 CMake 最低版本 cmake_minimum_required(VERSION 3.10) # 项目名称:cpp_tdd_add_demo project(cpp_tdd_add_demo) # 设置 C++ 标准(C++11 及以上均可) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 包含头文件目录(src/ 目录,后续存放业务头文件) include_directories(${PROJECT_SOURCE_DIR}/src) # 查找 GTest 库(系统全局安装,可直接找到) find_package(GTest REQUIRED) include_directories(${GTEST_INCLUDE_DIRS}) # 构建测试可执行文件:test_add(对应 test/test_add.cpp) add_executable(test_add test/test_add.cpp) # 链接 GTest 库和 pthread 库(GTest 依赖线程库) target_link_libraries(test_add ${GTEST_LIBRARIES} ${GTEST_MAIN_LIBRARIES} pthread)保存退出。
2.3 编译并运行测试(预期失败)
执行以下命令构建并运行测试,验证「红阶段」:
# 创建构建目录 mkdir build && cd build # 生成 Makefile cmake .. # 编译 make -j4此时编译会报错(提示undefined reference to Add(int, int)),因为我们只声明了Add函数,没有实现业务代码,这是「红阶段」的正常现象(测试用例无法运行,更无法通过)。
循环2:绿(Green)- 编写最简化业务代码让测试通过
核心:仅编写刚好能让测试用例通过的业务代码,不做多余设计。
2.4 编写业务代码头文件
进入src/目录,创建add.h头文件:
vim src/add.h粘贴以下内容(声明加法函数):
#ifndef ADD_H // 防止头文件重复包含(C++ 工程规范) #define ADD_H #include <climits> // 引入 INT_MAX/INT_MIN 定义 // 加法函数声明 int Add(int a, int b); #endif // ADD_H2.5 编写业务代码实现文件
在src/目录下创建add.cpp,实现Add函数:
vim src/add.cpp粘贴以下最简化实现代码(仅满足测试用例要求,不考虑优化):
// 引入头文件 #include "add.h" // 加法函数实现:最简化逻辑,直接返回 a + b int Add(int a, int b) { return a + b; }2.6 更新 CMakeLists.txt(包含业务代码)
修改add_demo/CMakeLists.txt,将src/add.cpp加入编译:
vim ../CMakeLists.txt # 若在 build 目录下,需回到上一级找到add_executable(test_add test/test_add.cpp),修改为:
# 包含业务代码 add.cpp,一起编译 add_executable(test_add test/test_add.cpp src/add.cpp)2.7 重新编译并运行测试(预期成功)
# 若在 build 目录下,直接执行 make -j4 # 运行测试可执行文件 ./test_add此时会输出 GTest 测试结果,若看到[ PASSED ] 2 tests.,说明测试用例全部通过,「绿阶段」完成!典型成功输出:
[==========] Running 2 tests from 1 test suite. [----------] Global test environment set-up. [----------] 2 tests from AddFunctionTest [ RUN ] AddFunctionTest.NormalInputTest [ OK ] AddFunctionTest.NormalInputTest (0 ms) [ RUN ] AddFunctionTest.BoundaryInputTest [ OK ] AddFunctionTest.BoundaryInputTest (0 ms) [----------] 2 tests from AddFunctionTest (0 ms total) [==========] 2 tests passed.循环3:重构(Refactor)- 优化代码且保持测试通过
核心:修改代码结构,不改变功能,测试用例始终保持通过。
当前的Add函数逻辑简单,可优化的点不多,我们以「增加异常处理(避免整数溢出)+ 规范代码注释 + 优化代码可读性」为例进行重构。
2.8 重构业务代码(add.cpp)
修改src/add.cpp,增加整数溢出判断,优化代码:
#include "add.h" #include <stdexcept> // 引入异常处理头文件 /** * @brief 整数加法函数,支持正常整数相加,避免溢出 * @param a 第一个整数 * @param b 第二个整数 * @return 两数之和 * @throw std::overflow_error 若相加结果溢出则抛出异常 */ int Add(int a, int b) { // 重构:增加整数溢出判断(避免 undefined behavior) // 情况1:正数 + 正数 溢出 if (a > 0 && b > 0 && a > INT_MAX - b) { throw std::overflow_error("Positive integer overflow"); } // 情况2:负数 + 负数 溢出 if (a < 0 && b < 0 && a < INT_MIN - b) { throw std::overflow_error("Negative integer overflow"); } // 原有功能逻辑不变,保证测试用例通过 return a + b; }2.9 重构测试用例(补充异常测试)
修改test/test_add.cpp,增加溢出异常的测试用例(TDD 重构阶段可补充完善测试用例,覆盖新的场景):
// 新增测试用例:测试溢出异常 TEST(AddFunctionTest, OverflowTest) { // EXPECT_THROW:预期函数抛出指定类型的异常,否则测试失败 EXPECT_THROW(Add(INT_MAX, 1), std::overflow_error); EXPECT_THROW(Add(INT_MIN, -1), std::overflow_error); }2.10 重新编译并运行测试(确保重构后测试通过)
# 编译(build 目录下) make -j4 # 运行测试 ./test_add此时应看到[ PASSED ] 3 tests.,说明重构后的代码功能正常,且新增了异常处理,代码质量更高,「重构阶段」完成。
四、TDD 核心补充(C++ 专属)
1. GTest 常用断言(新手必备)
TDD 中核心是通过断言判断功能是否符合预期,GTest 提供了两类断言,优先使用EXPECT_*:
| 断言类型 | 功能 | 示例 |
|---|---|---|
EXPECT_EQ(a, b) | 预期 a == b(相等) | EXPECT_EQ(Add(1,2),3) |
EXPECT_NE(a, b) | 预期 a != b(不相等) | EXPECT_NE(Add(1,2),4) |
EXPECT_GT(a, b) | 预期 a > b(大于) | EXPECT_GT(Add(2,3),4) |
EXPECT_LT(a, b) | 预期 a < b(小于) | EXPECT_LT(Add(1,2),4) |
EXPECT_THROW(func, ex) | 预期 func 抛出 ex 异常 | EXPECT_THROW(Add(INT_MAX,1), std::overflow_error) |
2. C++ TDD 项目的最佳实践
分离业务代码与测试代码:始终保持
src/(业务)和test/(测试)目录分离,避免测试代码污染业务代码;测试用例独立无依赖:每个测试用例不依赖其他用例的执行结果,不依赖外部环境(如文件、数据库),可通过「模拟对象(Mock)」隔离外部依赖(GTest 可结合 GMock 实现);
小步迭代:每次只实现一个最小功能单元(如先实现正常输入的加法,再实现边界值,最后实现异常处理);
重构优先保证测试通过:重构过程中可多次执行
./test_add,确保所有测试用例始终通过,避免引入新 Bug;自动化构建:结合 CI/CD 工具(如 Jenkins),每次提交代码自动编译并运行测试用例,实现持续验证。
3. 常见误区
先写业务代码再补测试:违背 TDD 「测试先行」原则,失去 TDD 驱动设计的意义;
测试用例覆盖不全:只测试正常场景,忽略边界值、异常场景,导致代码鲁棒性不足;
重构阶段修改功能:重构只优化代码结构,不改变功能,若需新增功能,应重新进入「红-绿-重构」循环。
五、总结
环境搭建核心:编译安装 GTest 并配置 CMake,实现测试代码与业务代码的编译链接;
TDD 核心循环:红(编写失败测试用例)→ 绿(编写最简业务代码让测试通过)→ 重构(优化代码且保持测试通过),无限重复直至功能完成;
C++ 专属要点:使用 GTest 断言判断结果,分离业务/测试目录,重构时增加异常处理等工程化优化;
入门关键:从简单功能(如加法、减法)入手,熟悉 GTest 用法和 TDD 循环,再逐步推进到复杂功能。
这个示例完成后,你可以尝试扩展功能(如实现减法、乘法函数),按照相同的 TDD 循环落地,逐步掌握 C++ TDD 的核心思维。