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()之前。这样结构清晰,一眼就能看到测试开关在哪。

注意:如果你在子目录的CMakeLists.txt里调用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。这样只有失败的测试才会打印详细信息,日志不会太臃肿。

小技巧:CTest支持-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_EXPRESSIONFAIL_REGULAR_EXPRESSION属性。你可以指定测试输出必须包含或不能包含某些正则表达式。比如:

set_tests_properties(test_math PROPERTIES
    PASS_REGULAR_EXPRESSION "All tests passed"
    FAIL_REGULAR_EXPRESSION "ERROR;FAILED"
)

这样CTest会检查测试输出,如果没看到"All tests passed"就算失败。嗯,这个功能我很少用,因为大多数测试框架自己就有断言。但如果你在跑shell脚本测试,它就很香了。

核心要点:测试属性让你不用改测试代码,就能控制测试行为。超时、环境变量、工作目录这些,全在CMake里配好,测试代码只管业务逻辑。

知识体系总览

下面这张图把本章的核心逻辑串起来了。从开启测试到注册用例,再到CTest执行和属性控制,一条线走到底。

CMake测试体系核心流程 enable_testing() add_test(NAME ... COMMAND ...) set_tests_properties(... PROPERTIES ...) ctest 运行 测试通过 / 失败 夹具 setup / cleanup 属性控制:TIMEOUT / ENVIRONMENT / LABELS / FIXTURES_REQUIRED

说白了,CMake的测试体系就这几步:开开关、注册用例、设属性、跑CTest。你不需要学太多花哨的东西,把这四个环节吃透,日常项目的测试管理就够用了。

我个人建议,新项目一开始就把enable_testing()和几个核心测试加上。哪怕只测一个函数,也比没有强。测试这东西,越早引入,后面越省心。

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