19、CTest与测试:enable_testing()与add_test()、ctest命令、GoogleTest集成、测试标签与超时设置
说实话,测试这件事,我以前也走过弯路。
刚用CMake那会儿,我觉得测试就是跑个可执行文件看看结果。后来项目大了,手动测试根本跑不过来。有一次改了个底层模块,自以为没问题,结果把三个同事的功能全搞崩了。从那以后,我老老实实把CTest用了起来。
CTest是CMake自带的测试工具。它不复杂,但很实用。说白了,它就是个测试调度器——帮你注册测试、批量运行、汇总结果。
19.1 开启测试:enable_testing()
想用CTest,第一步就是在CMakeLists.txt里调用enable_testing()。这个函数必须在顶级CMakeLists.txt里调用,子目录里调了也没用。
cmake_minimum_required(VERSION 3.16)
project(MyProject LANGUAGES CXX)
enable_testing() # 开启测试支持
add_subdirectory(src)
add_subdirectory(tests)
我个人习惯把enable_testing()放在项目定义之后、子目录添加之前。这样整个项目都能看到测试配置。
enable_testing()必须在add_subdirectory()之前调用,否则子目录里的add_test()会失效。我曾经在这个坑里浪费过半小时。
19.2 注册测试:add_test()
add_test()用来把可执行文件注册为测试用例。基本用法很简单:
add_test(NAME MyTest COMMAND my_test_executable)
NAME是测试名称,COMMAND是要运行的命令。你也可以传参数:
add_test(NAME MathTest COMMAND test_math --verbose)
实际项目中,我通常这样组织测试目录:
# tests/CMakeLists.txt
add_executable(test_math test_math.cpp)
target_link_libraries(test_math PRIVATE MyLib)
add_test(NAME TestMathAdd COMMAND test_math --add)
add_test(NAME TestMathSub COMMAND test_math --sub)
add_test(NAME TestMathMul COMMAND test_math --mul)
每个测试独立注册,失败时能精确定位问题。
19.3 运行测试:ctest命令
配置好测试后,用ctest命令运行。常用选项:
| 选项 | 说明 | 示例 |
|---|---|---|
| -N | 列出测试,不运行 | ctest -N |
| -R | 按正则匹配测试名 | ctest -R Math |
| -E | 排除匹配的测试 | ctest -E Slow |
| -j | 并行运行测试数 | ctest -j4 |
| --output-on-failure | 失败时输出详细信息 | ctest --output-on-failure |
| -V | 详细输出 | ctest -V |
我常用的组合是:
# 并行运行,失败时看详情
ctest -j4 --output-on-failure
# 只跑数学相关的测试
ctest -R Math -V
--output-on-failure。这样测试挂了能直接看到日志,不用再手动重跑。
19.4 GoogleTest集成
GoogleTest是C++社区最流行的测试框架。CMake对它的支持很完善。
首先,把GoogleTest源码或预编译包引入项目。我推荐用FetchContent:
include(FetchContent)
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/release-1.12.1.tar.gz
)
FetchContent_MakeAvailable(googletest)
然后写测试文件:
// test_math.cpp
#include <gtest/gtest.h>
#include "math.h"
TEST(MathTest, Add) {
EXPECT_EQ(add(1, 2), 3);
EXPECT_EQ(add(-1, 1), 0);
}
TEST(MathTest, Sub) {
EXPECT_EQ(sub(5, 3), 2);
EXPECT_EQ(sub(0, 0), 0);
}
在CMake里注册:
add_executable(test_math test_math.cpp)
target_link_libraries(test_math PRIVATE MyLib gtest_main)
# 关键一步:让CTest识别GoogleTest的测试用例
include(GoogleTest)
gtest_discover_tests(test_math)
gtest_discover_tests()会自动扫描可执行文件里的TEST宏,为每个测试用例生成独立的CTest条目。这样在ctest输出里能看到每个用例的通过/失败状态。
19.5 测试标签与超时设置
项目大了,测试会越来越多。有些测试跑得快,有些跑得慢。有些是单元测试,有些是集成测试。这时候标签就派上用场了。
设置标签
add_test(NAME UnitTest1 COMMAND test_unit1)
set_tests_properties(UnitTest1 PROPERTIES LABELS "unit;fast")
add_test(NAME IntegrationTest COMMAND test_integration)
set_tests_properties(IntegrationTest PROPERTIES LABELS "integration;slow")
运行指定标签的测试:
# 只跑单元测试
ctest -L unit
# 排除慢测试
ctest -LE slow
我在项目里通常分三个标签:unit(单元测试)、integration(集成测试)、smoke(冒烟测试)。CI里先跑smoke,过了再跑全部。
设置超时
有些测试可能死循环或卡住。设置超时能避免整个测试流程卡死:
add_test(NAME HeavyTest COMMAND test_heavy)
set_tests_properties(HeavyTest PROPERTIES TIMEOUT 30)
超时单位是秒。超过30秒,CTest会杀掉这个测试进程,标记为失败。
我一般给不同标签设不同超时:
# 单元测试5秒
set_tests_properties(UnitTest1 UnitTest2 PROPERTIES TIMEOUT 5)
# 集成测试60秒
set_tests_properties(IntegrationTest PROPERTIES TIMEOUT 60)
19.6 知识体系总览
下面这张图总结了CTest的核心流程和组件关系:
从图里能看出来,CTest的流程很清晰:先在CMake里配置测试,然后用ctest命令运行,最后看结果。标签和超时是辅助手段,帮你管理测试集。
19.7 实战建议
最后分享几个我在项目中积累的经验:
- 测试要快——单元测试最好秒级完成。超过10秒的测试,考虑移到集成测试标签里。
- 标签要规范——团队统一标签命名。我们团队用
unit、integration、smoke、benchmark四个标签。 - 超时别忘设——尤其是CI环境,一个卡死的测试可能拖垮整个流水线。
- GoogleTest是首选——如果项目用C++,我强烈推荐GoogleTest。CMake的集成支持很成熟,社区也活跃。
嗯,CTest的内容就这些。它不花哨,但很可靠。把测试管好了,项目质量才有保障。