3、搭建第一个测试环境:使用CMake + Google Test搭建C++单元测试项目,编写第一个测试用例
说实话,很多C++开发者对单元测试的态度是「知道很重要,但就是懒得搭」。我以前也这样,觉得写测试比写代码还麻烦。直到有一次,我重构一个模块时改了一行看似无关的代码,结果把三个功能同时搞崩了——测试用例跑完的那一刻,我后背都凉了。从那以后,我再也不敢「裸奔」写代码了。
这一章,我们就来亲手搭一个测试环境。你不需要任何花哨的工具,只需要CMake、Google Test,以及一点点耐心。
3.1 为什么选CMake + Google Test?
市面上测试框架不少,Catch2、Boost.Test、Google Test各有千秋。我个人习惯用Google Test,原因有三:
- 生态成熟:Google内部用了这么多年,坑基本都被踩平了。
- 断言丰富:从简单的EXPECT_EQ到复杂的EXPECT_THROW,覆盖了绝大多数场景。
- CMake集成方便:通过FetchContent或find_package,几行代码就能拉下来。
至于CMake,它已经是C++项目的标配了。你想想看,如果还在用Makefile手写依赖,那维护成本得多高?
3.2 项目结构设计
我建议你从一开始就保持清晰的项目结构。别学我早期那样,把所有文件塞到一个目录里——后来找测试文件找到崩溃。
一个典型的TDD项目结构长这样:
my_project/
├── CMakeLists.txt # 顶层CMake配置
├── src/ # 生产代码
│ ├── CMakeLists.txt
│ ├── calculator.h
│ └── calculator.cpp
├── tests/ # 测试代码
│ ├── CMakeLists.txt
│ └── test_calculator.cpp
└── third_party/ # 第三方依赖(可选)
└── googletest/ # 通过FetchContent自动下载
嗯,这里要注意:测试代码和生产代码一定要分开目录。这不是洁癖,是工程纪律。我曾经在一个项目里看到测试文件和源文件混在一起,结果CI脚本都不知道该编译哪些文件。
3.3 编写顶层CMakeLists.txt
我们先从顶层开始。这个文件负责两件事:定义项目、引入Google Test。
cmake_minimum_required(VERSION 3.14)
project(MyTddProject VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 启用测试
enable_testing()
# 引入Google Test(自动下载)
include(FetchContent)
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/refs/tags/v1.14.0.zip
)
FetchContent_MakeAvailable(googletest)
# 添加子目录
add_subdirectory(src)
add_subdirectory(tests)
这里有个小技巧:enable_testing() 一定要写在 add_subdirectory(tests) 之前,否则CTest不会注册测试用例。我当初就因为这个bug排查了半天,最后发现是顺序问题。
3.4 生产代码:一个简单的计算器
为了演示,我们写一个极简的计算器类。注意,这里我们只写接口和实现,不写测试——TDD讲究「先写测试再写代码」,但搭建环境阶段可以灵活一点。
src/calculator.h
#ifndef CALCULATOR_H
#define CALCULATOR_H
class Calculator {
public:
int Add(int a, int b);
int Subtract(int a, int b);
};
#endif
src/calculator.cpp
#include "calculator.h"
int Calculator::Add(int a, int b) {
return a + b;
}
int Calculator::Subtract(int a, int b) {
return a - b;
}
src/CMakeLists.txt
add_library(calculator
calculator.cpp
calculator.h
)
target_include_directories(calculator PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
3.5 测试代码:第一个测试用例
终于到了最激动人心的环节——写第一个测试用例。我会把测试文件拆成三部分:包含头文件、定义测试套件、编写测试用例。
tests/test_calculator.cpp
#include <gtest/gtest.h>
#include "calculator.h"
// 测试Add方法
TEST(CalculatorTest, AddReturnsSum) {
Calculator calc;
EXPECT_EQ(calc.Add(2, 3), 5);
EXPECT_EQ(calc.Add(-1, 1), 0);
EXPECT_EQ(calc.Add(0, 0), 0);
}
// 测试Subtract方法
TEST(CalculatorTest, SubtractReturnsDifference) {
Calculator calc;
EXPECT_EQ(calc.Subtract(5, 3), 2);
EXPECT_EQ(calc.Subtract(0, 0), 0);
EXPECT_EQ(calc.Subtract(-1, -1), 0);
}
tests/CMakeLists.txt
add_executable(test_calculator
test_calculator.cpp
)
target_link_libraries(test_calculator
PRIVATE
calculator
gtest_main
)
# 注册测试
add_test(NAME test_calculator COMMAND test_calculator)
这里有个关键点:gtest_main 库提供了 main() 函数,你不需要自己写。如果你有特殊的初始化逻辑,可以链接 gtest 然后自己写 main(),但新手阶段直接用 gtest_main 最省事。
3.6 编译与运行
一切就绪,我们来编译并运行测试。
# 在项目根目录下
mkdir build && cd build
cmake ..
cmake --build .
# 运行测试
ctest --output-on-failure
# 或者直接运行测试可执行文件
./tests/test_calculator
如果一切顺利,你会看到类似这样的输出:
[==========] Running 2 tests from 1 test suite.
[----------] Global test environment set-up.
[----------] 2 tests from CalculatorTest
[ RUN ] CalculatorTest.AddReturnsSum
[ OK ] CalculatorTest.AddReturnsSum (0 ms)
[ RUN ] CalculatorTest.SubtractReturnsDifference
[ OK ] CalculatorTest.SubtractReturnsDifference (0 ms)
[----------] 2 tests from CalculatorTest (0 ms total)
[==========] 2 tests from 1 test suite ran. (0 ms total)
[ PASSED ] 2 tests.
看到「PASSED」两个字,是不是很有成就感?
3.7 核心逻辑流程图
为了让你更直观地理解整个流程,我画了一张图:
3.8 避坑指南
搭建过程中,有几个坑我帮你提前踩了:
⚠️ 常见问题与解决方案
- 找不到gtest头文件:检查FetchContent是否成功下载。如果网络不行,可以手动下载并解压到third_party目录,然后用add_subdirectory引入。
- 链接错误:确保target_link_libraries中链接了gtest_main(不是gtest)。gtest不包含main函数。
- 测试没被CTest发现:检查CMakeLists.txt中是否调用了add_test,以及enable_testing是否在add_subdirectory之前。
💡 我的个人习惯
我习惯在tests/CMakeLists.txt中为每个测试文件单独创建target。虽然麻烦一点,但好处是:
- 编译时可以并行构建多个测试
- 某个测试挂了不会影响其他测试的编译
- CI中可以单独运行某个测试文件
3.9 验证你的环境
最后,我们来确认一下环境是否真的搭好了。你可以故意改错一个测试用例,比如把 EXPECT_EQ(calc.Add(2, 3), 5) 改成 EXPECT_EQ(calc.Add(2, 3), 6),然后重新编译运行。如果看到类似这样的输出:
[ FAILED ] CalculatorTest.AddReturnsSum (0 ms)
恭喜你,测试框架在正常工作!它成功发现了「错误」。
嗯,到这里,你的第一个C++单元测试环境就搭建完成了。从下一章开始,我们会真正进入TDD的实战环节——先写测试,再写代码,让测试驱动你的设计。
公众号:蓝海资料掘金营,微信deep3321