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
小技巧:在CI脚本里,我习惯加--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输出里能看到每个用例的通过/失败状态。

为什么用gtest_discover_tests?手动add_test只能注册整个可执行文件。如果test_math里有20个TEST用例,一个失败你就不知道是哪个。gtest_discover_tests帮你拆成20个独立测试,定位问题快得多。

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)
注意:超时时间别设太紧。我在一个项目里把单元测试超时设成2秒,结果CI经常因为机器负载高而误报失败。后来改成5秒就稳定了。给点余量,别太苛刻。

19.6 知识体系总览

下面这张图总结了CTest的核心流程和组件关系:

CTest测试体系核心流程 CMakeLists.txt 配置 enable_testing() add_test() / gtest_discover_tests() set_tests_properties() ctest 命令运行 并行运行 (-jN) 按标签筛选 (-L) 按名称匹配 (-R) 超时控制 (TIMEOUT) 测试报告 / 通过 / 失败 CMake配置 运行阶段 运行方式 输出结果

从图里能看出来,CTest的流程很清晰:先在CMake里配置测试,然后用ctest命令运行,最后看结果。标签和超时是辅助手段,帮你管理测试集。

19.7 实战建议

最后分享几个我在项目中积累的经验:

  • 测试要快——单元测试最好秒级完成。超过10秒的测试,考虑移到集成测试标签里。
  • 标签要规范——团队统一标签命名。我们团队用unitintegrationsmokebenchmark四个标签。
  • 超时别忘设——尤其是CI环境,一个卡死的测试可能拖垮整个流水线。
  • GoogleTest是首选——如果项目用C++,我强烈推荐GoogleTest。CMake的集成支持很成熟,社区也活跃。

嗯,CTest的内容就这些。它不花哨,但很可靠。把测试管好了,项目质量才有保障。


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