第十四章:测试框架集成——enable_testing()、add_test()、CTest与覆盖率

说实话,测试这件事,很多C++开发者一开始都不太重视。我早年带团队时,就见过一个项目,代码编译全过,一跑就崩,最后定位到是某个模块的边界条件没处理。从那以后,我就把测试集成写进了CMake的标配流程里。

这一章,我们来聊聊CMake里怎么把测试玩明白。说白了,就是三件事:注册测试、运行测试、看覆盖率

14.1 开启测试:enable_testing()

在CMake里,测试不是默认开启的。你得先告诉构建系统:“嘿,我要跑测试了。” 这个动作就是 enable_testing()

核心规则:enable_testing() 必须在顶级 CMakeLists.txt 中调用,且必须在 add_test() 之前。

我个人习惯把它放在项目配置之后、目标定义之前。举个例子:

cmake_minimum_required(VERSION 3.16)
project(MyApp VERSION 1.0.0 LANGUAGES CXX)

enable_testing()  # 开启测试支持

add_subdirectory(src)
add_subdirectory(tests)  # 测试代码单独放一个目录

为什么要在顶级目录调用?因为CTest需要知道整个项目的测试结构。如果你在子目录里调用 enable_testing(),CTest可能找不到测试用例。嗯,这里要注意,我见过有人踩过这个坑。

14.2 注册测试:add_test()

测试开启后,下一步就是告诉CMake:“这个可执行文件是一个测试用例。” 用 add_test() 来注册。

基本语法很简单:

add_test(NAME <test_name> COMMAND <executable> [arg1 arg2 ...])

举个例子,假设你写了一个单元测试 test_math

add_executable(test_math test_math.cpp)
target_link_libraries(test_math PRIVATE my_math_lib)

add_test(NAME MathTest COMMAND test_math)

这样,当你运行 ctest 时,它就会自动执行 test_math 这个程序。

小技巧: 测试名称最好有规律,比如 ModuleName_TestName。我在项目中就用 Math_AddTestMath_SubTest 这种命名,方便筛选和排错。

14.3 运行测试:CTest

测试注册好了,怎么跑?用 ctest 命令。它会在构建目录下扫描所有注册的测试,然后逐个执行。

基本用法:

# 先构建
cmake -B build
cmake --build build

# 再测试
cd build
ctest

输出大概长这样:

Test project /path/to/build
    Start 1: MathTest
1/1 Test #1: MathTest ......................   Passed    0.02 sec

100% tests passed, 0 tests failed out of 1

CTest 还支持很多参数,我常用的几个:

参数 作用
-V 显示详细输出,包括测试程序自己的打印
-N 只列出测试,不执行
-R 按正则表达式筛选测试名
-j 并行执行测试,比如 -j4
--output-on-failure 只在失败时输出详细信息

举个例子,我只想跑数学相关的测试:

ctest -R Math -V

这样只会执行名字里带 Math 的测试,而且会显示详细输出。调试时特别好用。

14.4 测试覆盖率

测试跑通了,但测了多少代码?这就是覆盖率要回答的问题。CMake 本身不直接生成覆盖率报告,但它可以配合 gcovlcov 来搞定。

基本步骤:

  1. 编译时开启覆盖率标志
  2. 运行测试
  3. gcovlcov 生成报告

在CMake里,我一般这样配置:

option(ENABLE_COVERAGE "Enable coverage reporting" OFF)

if(ENABLE_COVERAGE)
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage -g -O0")
    set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage")
endif()

然后构建时开启:

cmake -B build -DENABLE_COVERAGE=ON
cmake --build build
cd build
ctest

测试跑完后,用 lcov 生成HTML报告:

lcov --capture --directory . --output-file coverage.info
genhtml coverage.info --output-directory coverage_report

打开 coverage_report/index.html,就能看到哪些代码被覆盖了,哪些没被覆盖。

注意: 覆盖率不是越高越好。我见过有人为了100%覆盖率,写了一堆无意义的测试用例。说白了,覆盖率是工具,不是目标。重点测核心逻辑和边界条件。

14.5 知识体系总览

这一章的内容,我用一张图来总结:

CMake 测试框架集成知识体系 CMake 测试集成 enable_testing() 在顶级 CMakeLists.txt 调用 必须在 add_test 之前 add_test() NAME + COMMAND 参数 支持传递命令行参数 CTest 运行 -V 详细输出 -R 正则筛选 -j 并行执行 测试覆盖率 --coverage 编译标志 lcov + genhtml 生成报告

14.6 避坑指南

最后,分享几个我踩过的坑:

  • 测试路径问题: 测试程序的工作目录是构建目录,不是源码目录。如果你在测试里读取文件,记得用绝对路径或者通过CMake传递路径参数。
  • 并行测试冲突: 如果多个测试写同一个文件,并行执行时会互相干扰。我一般给每个测试分配独立的临时目录。
  • 覆盖率数据不更新: 有时候改了代码,覆盖率报告还是旧的。记得先 make clean 再重新构建,或者用 lcov --zerocounters 清零。

好了,这一章的内容就到这里。测试集成这件事,说白了就是“先注册、再运行、最后看覆盖”。把这三步走顺了,项目的质量就有了基本保障。


公众号:蓝海资料掘金营,微信deep3321