22、CMake进阶:测试与CTest:enable_testing, add_test的使用,CTest的配置与运行,测试覆盖率。
说实话,很多嵌入式工程师对测试的态度是「能跑就行」。我以前也这么想,直到有一次在量产前发现一个定时器中断的边界条件没覆盖到,差点导致整个批次返工。从那以后,我把测试放到了和编译同等重要的位置。
CMake 自带的 CTest 工具,说白了就是帮你把测试这件事「自动化」起来。你不用再手动敲命令去跑每个测试用例,也不用担心漏掉某个模块。咱们今天就把 CTest 的配置、运行和覆盖率讲透。
22.1 开启测试:enable_testing()
在 CMake 里启用测试,只需要在顶层 CMakeLists.txt 里加一行:
enable_testing()
嗯,就这么简单。但有个坑——这行必须写在顶层目录,不能写在子目录里。我刚开始用的时候,习惯性地把它放到了 test/CMakeLists.txt 里,结果 CTest 死活不认。后来查文档才发现,enable_testing() 只在当前目录及其子目录生效,而 CTest 默认从构建目录的顶层开始扫描。
22.2 注册测试:add_test()
有了测试框架,接下来就是把具体的测试用例注册进去。基本用法是:
add_test(NAME test_name COMMAND executable [args...])
举个例子,假设你有一个单元测试的可执行文件 test_uart:
add_executable(test_uart test_uart.c)
target_link_libraries(test_uart PRIVATE uart_lib)
add_test(NAME uart_basic_test COMMAND test_uart --basic)
add_test(NAME uart_baud_test COMMAND test_uart --baudrate 115200)
这里我习惯把测试分成多个 NAME,每个 NAME 对应一个测试场景。这样当某个测试失败时,CTest 能精确告诉你哪个场景出了问题,而不是笼统地报「test_uart 挂了」。
22.3 CTest 的配置与运行
配置好测试后,运行起来就很简单了。在构建目录下执行:
cmake --build .
ctest
CTest 默认会并行运行测试,输出结果类似:
Test project /home/user/build
Start 1: uart_basic_test
1/2 Test #1: uart_basic_test ............... Passed 0.12 sec
Start 2: uart_baud_test
2/2 Test #2: uart_baud_test ............... Passed 0.08 sec
100% tests passed, 0 tests failed out of 2
我个人常用的几个 CTest 参数:
| 参数 | 作用 | 我的使用场景 |
|---|---|---|
| -V | 显示详细输出 | 调试测试失败时必加 |
| -N | 只列出测试,不运行 | 检查测试是否注册成功 |
| -R | 按正则匹配测试名 | 只跑某个模块的测试 |
| -j | 并行数 | 嵌入式交叉编译时设 -j1 避免资源冲突 |
举个例子,我只想跑 UART 相关的测试:
ctest -R uart -V
这样就能看到每个 UART 测试的完整输出了。
22.4 测试覆盖率
覆盖率是个好东西,但别迷信它。我见过有人把覆盖率堆到 95%,结果核心逻辑还是漏了。覆盖率只能告诉你「哪些代码被执行了」,不能告诉你「执行得对不对」。
在 CMake 里集成覆盖率,通常用 gcov 配合 lcov。配置步骤:
- 编译时加上
-fprofile-arcs -ftest-coverage和--coverage - 运行测试生成
.gcda文件 - 用
lcov收集数据,生成 HTML 报告
在 CMake 里可以这样设置:
option(ENABLE_COVERAGE "Enable coverage build" OFF)
if(ENABLE_COVERAGE)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fprofile-arcs -ftest-coverage")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --coverage")
endif()
然后写个自定义目标来生成报告:
add_custom_target(coverage
COMMAND lcov --capture --directory . --output-file coverage.info
COMMAND genhtml coverage.info --output-directory coverage_report
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
COMMENT "Generating coverage report..."
)
运行:
cmake -DENABLE_COVERAGE=ON ..
make
ctest
make coverage
打开 coverage_report/index.html 就能看到每个文件的覆盖情况了。
22.5 知识体系总览
下面这张图总结了 CTest 的核心流程和覆盖率集成方式:
从图里能看出来,CTest 的核心就是三步:开启、注册、运行。覆盖率是锦上添花,不是必须的。我建议你先跑通基础测试,再考虑加覆盖率。
22.6 避坑指南
最后分享几个我踩过的坑:
- 测试可执行文件找不到:add_test 里的 COMMAND 必须是构建出来的可执行文件路径。如果你用了
add_subdirectory,记得检查目标是否在同一个构建目录下。 - 交叉编译环境下的测试:CTest 默认在宿主机上运行测试。如果你要测 ARM 目标板,得用
add_test配合--test-command或者自己写个脚本把测试文件传到板子上再跑。 - 覆盖率文件被覆盖:多次运行测试时,
.gcda文件会累积数据。如果你只想测一次,记得先清理:find . -name "*.gcda" -delete。
嗯,CTest 的内容差不多就这些。说白了,它就是个帮你自动跑测试的工具,配置起来不复杂,但用好了能省不少心。下次咱们聊聊更高级的测试框架集成,比如 Google Test 怎么和 CMake 配合。