16、测试:enable_testing、add_test命令、CTest集成、测试夹具、测试属性
测试这件事,说实话,很多做C++的朋友一开始都不太重视。我早年也是这样——觉得写测试浪费时间,代码能跑就行。直到有一次,我改了一个底层数据结构,结果把三个模块的逻辑全搞崩了,排查了两天才找到原因。从那以后,我再也不敢不写测试了。
CMake对测试的支持其实很成熟。它不要求你用特定的测试框架,也不规定你怎么组织测试用例。它只提供一套基础设施,让你能把测试跑起来、跑得顺、跑得明白。
16.1 enable_testing:开启测试的大门
想用CMake做测试,第一步就是调用enable_testing()。这个命令必须在顶级CMakeLists.txt里调用,而且要在project()之后。为什么?因为CMake需要知道整个项目的测试范围,子目录里的测试也要归它管。
cmake_minimum_required(VERSION 3.16)
project(MyApp VERSION 1.0)
enable_testing()
add_subdirectory(src)
add_subdirectory(tests)
我个人习惯把enable_testing()放在project()之后、add_subdirectory()之前。这样结构清晰,一眼就能看到测试开关在哪。
enable_testing(),CMake会报错。它只能在顶级目录调用一次。
16.2 add_test:注册你的测试用例
add_test()是注册测试的核心命令。它的基本用法很简单:
add_test(NAME test_math COMMAND test_math_executable)
这里NAME是测试的名字,COMMAND后面跟的是可执行文件。你还可以传参数:
add_test(NAME test_with_args COMMAND test_runner --config debug --suite math)
我在项目中遇到过一个问题:测试可执行文件路径不对。因为CMake默认会在构建目录里找可执行文件,如果你用了自定义的输出目录,记得用$<TARGET_FILE:test_math_executable>来获取完整路径。
add_test(NAME test_math
COMMAND $<TARGET_FILE:test_math_executable>)
这样写,CMake会自动替换成正确的路径,省心不少。
16.3 CTest集成:一键跑所有测试
CTest是CMake自带的测试工具。你不需要额外安装,只要在构建目录里运行ctest命令就行。
# 配置和构建
mkdir build && cd build
cmake ..
cmake --build .
# 运行所有测试
ctest
# 输出详细信息
ctest -V
# 并行运行测试(4个任务)
ctest -j4
CTest会读取CMakeCache里的测试列表,然后逐个运行。默认情况下,它只显示测试通过还是失败。加上-V(verbose)就能看到每个测试的完整输出。
我建议你在CI脚本里用ctest --output-on-failure。这样只有失败的测试才会打印详细信息,日志不会太臃肿。
-R和-E参数来过滤测试。-R math只运行名字包含"math"的测试,-E slow排除名字包含"slow"的测试。调试时特别有用。
16.4 测试夹具:set_tests_properties与FIXTURES
测试夹具(Fixture)是CMake 3.7引入的功能。它解决了一个实际问题:有些测试需要前置条件,比如启动数据库、创建临时文件、设置环境变量。
夹具由三部分组成:
- FIXTURES_SETUP:前置任务,在测试前运行
- FIXTURES_CLEANUP:清理任务,在测试后运行
- FIXTURES_REQUIRED:测试依赖的夹具名称
看个例子:
# 启动数据库的脚本
add_test(NAME setup_db
COMMAND ${CMAKE_COMMAND} -E echo "Starting database...")
set_tests_properties(setup_db PROPERTIES
FIXTURES_SETUP my_db_fixture)
# 清理数据库的脚本
add_test(NAME cleanup_db
COMMAND ${CMAKE_COMMAND} -E echo "Stopping database...")
set_tests_properties(cleanup_db PROPERTIES
FIXTURES_CLEANUP my_db_fixture)
# 真正的测试
add_test(NAME test_user_query
COMMAND test_user_query)
set_tests_properties(test_user_query PROPERTIES
FIXTURES_REQUIRED my_db_fixture)
当你运行test_user_query时,CTest会自动先跑setup_db,再跑test_user_query,最后跑cleanup_db。你不用手动排序,CTest全包了。
我曾经在项目里用夹具来管理测试数据库。每个测试用例都依赖一个db_fixture,setup里创建测试表,cleanup里删掉。这样测试之间互不干扰,跑完干干净净。
16.5 测试属性:精细控制每个测试
set_tests_properties()可以设置很多属性。除了夹具,常用的还有:
| 属性名 | 作用 | 示例 |
|---|---|---|
| TIMEOUT | 超时时间(秒) | TIMEOUT 30 |
| ENVIRONMENT | 设置环境变量 | ENVIRONMENT "DB_HOST=localhost;DB_PORT=5432" |
| WORKING_DIRECTORY | 工作目录 | WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/data" |
| LABELS | 标签,用于分组 | LABELS "unit" "math" |
| COST | 测试开销(越大越晚跑) | COST 10 |
| REQUIRED_FILES | 依赖的文件 | REQUIRED_FILES "test_data.csv" |
举个例子,一个需要环境变量和超时控制的测试:
add_test(NAME test_api COMMAND test_api)
set_tests_properties(test_api PROPERTIES
TIMEOUT 60
ENVIRONMENT "API_KEY=test_key;API_URL=http://localhost:8080"
LABELS "integration" "api"
)
CTest还支持PASS_REGULAR_EXPRESSION和FAIL_REGULAR_EXPRESSION属性。你可以指定测试输出必须包含或不能包含某些正则表达式。比如:
set_tests_properties(test_math PROPERTIES
PASS_REGULAR_EXPRESSION "All tests passed"
FAIL_REGULAR_EXPRESSION "ERROR;FAILED"
)
这样CTest会检查测试输出,如果没看到"All tests passed"就算失败。嗯,这个功能我很少用,因为大多数测试框架自己就有断言。但如果你在跑shell脚本测试,它就很香了。
知识体系总览
下面这张图把本章的核心逻辑串起来了。从开启测试到注册用例,再到CTest执行和属性控制,一条线走到底。
说白了,CMake的测试体系就这几步:开开关、注册用例、设属性、跑CTest。你不需要学太多花哨的东西,把这四个环节吃透,日常项目的测试管理就够用了。
我个人建议,新项目一开始就把enable_testing()和几个核心测试加上。哪怕只测一个函数,也比没有强。测试这东西,越早引入,后面越省心。