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 核心逻辑流程图

为了让你更直观地理解整个流程,我画了一张图:

CMake + Google Test 测试环境搭建流程 1. 创建项目结构 src/ tests/ CMakeLists.txt 2. 编写顶层CMake 引入Google Test 3. 编写生产代码 calculator.h/.cpp 4. 编写 测试用例 5. 编译项目 cmake --build . 6. 运行测试 ctest 或直接运行 通过? 修复代码 ✅ 测试全部通过

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