一、CMake测试体系:从零到自动化

测试这件事,说实话,很多C/C++项目做得并不好。我见过太多项目,代码写得漂亮,但一说到测试就头疼。要么是手动跑测试,要么是测试脚本写得比代码还复杂。CMake的CTest模块,就是为了解决这个问题而生的。

我个人习惯在项目一开始就把测试框架搭好。为什么呢?因为后期再补测试,成本高得吓人。你想想看,一个项目几十万行代码,要回头加测试,光是梳理依赖关系就够你喝一壶的。

1.1 enable_testing():开启测试的开关

这个函数很简单,但很重要。它告诉CMake:嘿,我这个项目要跑测试了。

# 在顶层CMakeLists.txt中
cmake_minimum_required(VERSION 3.10)
project(MyProject C CXX)

enable_testing()  # 开启测试支持

add_subdirectory(src)
add_subdirectory(tests)

注意,enable_testing()必须在顶层CMakeLists.txt中调用。我见过有人把它放在子目录里,结果CTest根本找不到测试。嗯,这是个坑。

避坑指南:我曾经在一个大型项目中,把enable_testing()放在了tests子目录里。结果折腾了半天,CTest始终报"No tests were found"。后来才发现,这个函数必须在顶层调用,子目录里调用是无效的。

1.2 add_test():注册你的测试用例

有了测试开关,接下来就是注册测试用例了。add_test()就是干这个的。

# tests/CMakeLists.txt
# 先编译测试可执行文件
add_executable(test_math test_math.cpp)
target_link_libraries(test_math PRIVATE mylib)

# 注册测试
add_test(NAME test_math_basic COMMAND test_math)
add_test(NAME test_math_advanced COMMAND test_math --advanced)

这里有个细节:NAME参数是测试的名字,COMMAND是运行测试的命令。你可以给同一个可执行文件传不同的参数,注册成多个测试用例。

我建议测试命名要有规律。比如用模块名_功能名_场景名的格式。这样当测试失败时,你一眼就能看出是哪个模块出了问题。

1.3 CTest配置:让测试跑得更聪明

光有测试还不够,你得告诉CTest怎么跑。比如并行测试、超时控制、测试标签等。

# 在CMakeLists.txt中配置CTest
set(CTEST_PARALLEL_LEVEL 4)  # 并行跑4个测试
set(CTEST_TEST_TIMEOUT 60)   # 每个测试超时60秒

# 给测试打标签
add_test(NAME test_unit_math COMMAND test_math)
set_tests_properties(test_unit_math PROPERTIES
    LABELS "unit;math"
    TIMEOUT 30
    COST 2.0
)

为什么要打标签?因为你可以只跑特定标签的测试。比如提交代码前只跑unit标签的单元测试,晚上再跑integration标签的集成测试。

我的经验:在CI流水线中,我通常把测试分成三级:快速检查(<5分钟)、完整测试(<30分钟)、深度测试(>1小时)。用标签来区分,CI可以灵活选择跑哪些测试。

二、测试覆盖率:gcov + lcov 实战

测试覆盖率这东西,说白了就是看看你的测试到底覆盖了多少代码。我见过有人拍胸脯说测试很完善,结果一跑覆盖率,发现核心函数根本没测到。

2.1 开启覆盖率编译

gcov是GCC自带的覆盖率工具。要使用它,编译时需要加特殊参数。

# CMakeLists.txt中配置覆盖率
option(ENABLE_COVERAGE "Enable coverage reporting" OFF)

if(ENABLE_COVERAGE)
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage -fprofile-arcs -ftest-coverage")
    set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage")
endif()

注意,--coverage是GCC的简写,等价于-fprofile-arcs -ftest-coverage。我个人习惯用--coverage,少打几个字嘛。

2.2 生成覆盖率报告

跑完测试后,会生成.gcda.gcno文件。然后用lcov生成HTML报告。

# 生成覆盖率报告的命令
# 1. 收集原始数据
lcov --capture --directory . --output-file coverage.info

# 2. 过滤掉系统头文件和测试代码
lcov --remove coverage.info '/usr/*' '*/tests/*' --output-file coverage_filtered.info

# 3. 生成HTML报告
genhtml coverage_filtered.info --output-directory coverage_report

这里有个坑:如果不过滤系统头文件,报告里会包含一大堆/usr/include下的代码,根本没法看。我刚开始用lcov时就犯过这个错,生成了一份几千页的报告,全是系统库的覆盖率。

关键指标:
  • 行覆盖率(Line Coverage):代码行被执行的比例
  • 分支覆盖率(Branch Coverage):if/else等分支被覆盖的比例
  • 函数覆盖率(Function Coverage):函数被调用的比例

我个人认为,行覆盖率到80%以上算及格,90%以上算良好。但别盲目追求100%,有些错误处理代码很难触发。

三、CDash集成:让测试结果可视化

CDash是一个Web平台,用来收集和展示测试结果。你可以把它想象成测试报告的仪表盘。

3.1 配置CDash提交

要让CTest把结果提交到CDash,需要配置CTestConfig.cmake文件。

# CTestConfig.cmake
set(CTEST_PROJECT_NAME "MyProject")
set(CTEST_NIGHTLY_START_TIME "00:00:00 EST")

set(CTEST_DROP_METHOD "http")
set(CTEST_DROP_SITE "my-cdash-server.com")
set(CTEST_DROP_LOCATION "/submit.php?project=MyProject")
set(CTEST_DROP_SITE_CDASH TRUE)

然后在CI脚本中调用CTest:

# 提交到CDash的命令
ctest -D Experimental  # 实验性构建
ctest -D Nightly       # 每日构建
ctest -D Continuous    # 持续构建

三种模式的区别:

模式 用途 我什么时候用
Experimental 开发人员手动提交测试结果 本地验证新功能时
Nightly 每天固定时间自动构建测试 团队每日集成检查
Continuous 代码提交后立即触发 CI流水线中

3.2 CDash能告诉你什么

CDash的界面会展示:

  • 哪些测试通过了,哪些失败了
  • 测试耗时变化趋势
  • 覆盖率变化曲线
  • 不同平台、不同编译器的测试结果对比

我记得有一次,CDash显示某个测试在Linux上全过,但在Windows上全挂。一看日志,原来是路径分隔符的问题。这种跨平台问题,没有CDash很难及时发现。

四、知识体系总览

下面这张图,是我梳理的CMake测试体系的核心逻辑。你可以把它当作一个检查清单。

CMake测试体系核心逻辑 enable_testing() add_test() 注册测试用例 CTest配置 并行/超时/标签 gcov + lcov 覆盖率报告 CDash集成 结果可视化 测试报告 & 质量门禁

这张图展示了从开启测试到生成报告的全流程。说白了,就是三步:开启、注册、配置。但每一步都有很多细节,需要根据项目实际情况来调整。

我的建议:刚开始接触CTest时,别想着一步到位。先跑通最简单的测试,再逐步加上覆盖率、CDash。我见过太多人一开始就想搭个完美的测试体系,结果折腾两周还没跑通一个测试。

好了,这一章的内容就到这里。测试是个持续改进的过程,没有银弹。关键是让团队养成写测试、看覆盖率、关注测试结果的习惯。工具只是辅助,人才是核心。

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