CMake 从入门到精通:完整教程 / 第 10 章:测试与 CTest
第 10 章:测试与 CTest
10.1 CTest 简介
CTest(CTest)是 CMake 的测试驱动程序,用于自动化运行和管理测试。
CTest 架构
├── CMakeLists.txt 定义测试
├── CTestTestfile.cmake 生成的测试脚本
├── CTestCustom.cmake 自定义配置
└── Testing/ 测试结果输出
└── Temporary/
10.2 启用测试
# 顶层 CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(MyProject)
# 方式一:enable_testing()(推荐用于顶层)
enable_testing()
# 方式二:include(CTest)(提供额外选项)
include(CTest)
add_subdirectory(src)
add_subdirectory(tests)
⚠️ 注意:
enable_testing()必须在顶层CMakeLists.txt中调用。
10.3 定义测试
10.3.1 基本测试
# 创建测试可执行文件
add_executable(test_basic test_basic.cpp)
target_link_libraries(test_basic PRIVATE mylib)
# 注册测试
add_test(NAME BasicTest COMMAND test_basic)
10.3.2 带参数的测试
add_test(
NAME FileTest
COMMAND test_file --input data.txt --expected result.txt
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
)
10.3.3 使用生成器表达式
add_test(
NAME MyTest
COMMAND $<TARGET_FILE:test_basic> # 推荐使用生成器表达式
)
10.3.4 脚本测试
# Python 测试
find_package(Python3 REQUIRED)
add_test(
NAME PythonTest
COMMAND Python3::Interpreter ${CMAKE_SOURCE_DIR}/tests/test_script.py
)
# Shell 脚本测试
add_test(
NAME ShellTest
COMMAND bash ${CMAKE_SOURCE_DIR}/tests/test_shell.sh
)
# CMake 脚本测试
add_test(
NAME CMakeScriptTest
COMMAND ${CMAKE_COMMAND} -P ${CMAKE_SOURCE_DIR}/tests/test_cmake.cmake
)
10.4 测试属性
10.4.1 超时
add_test(NAME LongTest COMMAND test_long)
# 设置超时(秒)
set_tests_properties(LongTest PROPERTIES
TIMEOUT 30 # 单个测试超时
)
# 全局超时
# CTestCustom.cmake
set(CTEST_TEST_TIMEOUT 120)
10.4.2 标签(Labels)
# 为测试设置标签
add_test(NAME UnitTest COMMAND test_unit)
add_test(NAME IntegrationTest COMMAND test_integration)
add_test(NAME SlowTest COMMAND test_slow)
set_tests_properties(UnitTest PROPERTIES LABELS "unit;fast")
set_tests_properties(IntegrationTest PROPERTIES LABELS "integration")
set_tests_properties(SlowTest PROPERTIES LABELS "unit;slow")
# 按标签运行
# ctest -L unit # 运行所有 unit 测试
# ctest -L fast # 运行所有 fast 测试
# ctest -LE slow # 排除 slow 测试
# ctest -L "unit;fast" # 匹配任一标签
10.4.3 工作目录和环境
set_tests_properties(MyTest PROPERTIES
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}/test_output
ENVIRONMENT "MY_CONFIG_FILE=${CMAKE_SOURCE_DIR}/test.conf"
ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:${CMAKE_BINARY_DIR}/bin"
)
10.4.4 必须通过的测试
# 如果此测试失败,后续测试不会运行
set_tests_properties(CoreTest PROPERTIES
FIXTURES_SETUP core_fixture
)
set_tests_properties(DependsOnCore PROPERTIES
FIXTURES_REQUIRED core_fixture
)
10.4.5 预期失败
add_test(NAME KnownFailure COMMAND test_known_issue)
# 标记为预期失败
set_tests_properties(KnownFailure PROPERTIES
WILL_FAIL TRUE
)
10.4.6 成功条件
# 自定义成功条件(匹配输出)
set_tests_properties(MyTest PROPERTIES
PASS_REGULAR_EXPRESSION "All tests passed"
FAIL_REGULAR_EXPRESSION "ERROR"
)
10.4.7 测试属性汇总
| 属性 | 说明 | 示例 |
|---|---|---|
TIMEOUT | 超时秒数 | 30 |
LABELS | 标签列表 | "unit;fast" |
WORKING_DIRECTORY | 工作目录 | ${CMAKE_BINARY_DIR} |
ENVIRONMENT | 环境变量 | "KEY=VALUE" |
WILL_FAIL | 预期失败 | TRUE |
DISABLED | 禁用测试 | TRUE |
SKIP_RETURN_CODE | 跳过返回码 | 77 |
FIXTURES_SETUP | Fixture 设置 | "db_fixture" |
FIXTURES_REQUIRED | Fixture 需求 | "db_fixture" |
FIXTURES_CLEANUP | Fixture 清理 | "db_fixture" |
RESOURCE_LOCK | 资源锁 | "gpu" |
COST | 测试成本(排序用) | 5.0 |
PROCESSORS | 使用的处理器数 | 4 |
DEPENDS | 依赖的测试 | "OtherTest" |
RUN_SERIAL | 串行运行 | TRUE |
PARALLEL_LEVEL | 并行级别 | 4 |
10.5 使用 Google Test
10.5.1 集成 GTest
include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.14.0
)
# Windows 特殊处理
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)
enable_testing()
# 创建测试
add_executable(mytests
test_parser.cpp
test_utils.cpp
)
target_link_libraries(mytests PRIVATE
mylib
GTest::gtest
GTest::gtest_main
)
# 自动发现测试
include(GoogleTest)
gtest_discover_tests(mytests)
10.5.2 gtest_discover_tests
include(GoogleTest)
# 自动发现并注册所有 TEST
gtest_discover_tests(mytests
# 发现选项
DISCOVERY_TIMEOUT 30 # 发现超时
DISCOVERY_MODE PRE_TEST # PRE_TEST(默认)或 POST_BUILD
# 过滤
TEST_FILTER "Parser*" # 只运行匹配的测试
# 工作目录
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
# 额外属性
PROPERTIES
LABELS "unit"
TIMEOUT 10
)
10.5.3 使用 Catch2
include(FetchContent)
FetchContent_Declare(
Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.5.2
)
FetchContent_MakeAvailable(Catch2)
add_executable(tests test_main.cpp)
target_link_libraries(tests PRIVATE Catch2::Catch2WithMain)
include(Catch)
catch_discover_tests(tests)
10.6 运行测试
10.6.1 基本运行
# 构建并运行测试
cmake --build build
ctest --test-dir build
# 或在 build 目录中
cd build
ctest
# 显示输出
ctest --output-on-failure
# 详细输出
ctest -V
ctest --verbose
10.6.2 过滤测试
# 按名称过滤(正则表达式)
ctest -R "Parser.*"
ctest --tests-regex "Parser.*"
# 排除测试
ctest -E "Slow.*"
ctest --exclude-regex "Slow.*"
# 按标签过滤
ctest -L "unit"
ctest --label-regex "unit"
# 排除标签
ctest -LE "slow"
# 按索引运行
ctest -I 1,5 # 运行第 1-5 个测试
ctest -I ,,3 # 每 3 个为一组
10.6.3 并行运行
# 并行运行测试
ctest -j4 # 4 个并行
ctest --parallel 8 # 8 个并行
# 使用处理器
ctest --resource-spec-file resources.json
10.6.4 输出格式
# JUnit XML 输出(用于 CI)
ctest --output-junit results.xml
# CDash 输出
ctest -D Experimental
ctest -T Test
ctest -T Coverage
ctest -T MemCheck
10.6.5 测试总结
ctest --test-dir build --output-on-failure --parallel 4
# 输出示例:
# Test project /home/user/project/build
# Start 1: ParserTest.Basic
# Start 2: ParserTest.Advanced
# Start 3: UtilsTest.StringOps
# 1/3 Test #1: ParserTest.Basic .............. Passed 0.05 sec
# 2/3 Test #2: ParserTest.Advanced ........... Passed 0.12 sec
# 3/3 Test #3: UtilsTest.StringOps ........... Passed 0.03 sec
#
# 100% tests passed, 0 tests failed out of 3
#
# Total Test time (real) = 0.21 sec
10.7 测试覆盖率
10.7.1 启用覆盖率
option(ENABLE_COVERAGE "启用代码覆盖率" OFF)
if(ENABLE_COVERAGE)
# GCC/Clang 覆盖率标志
add_compile_options(--coverage -fprofile-arcs -ftest-coverage)
add_link_options(--coverage)
endif()
10.7.2 使用 lcov
# 1. 构建并启用覆盖率
cmake -S . -B build -DENABLE_COVERAGE=ON -DCMAKE_BUILD_TYPE=Debug
# 2. 运行测试
cmake --build build
ctest --test-dir build
# 3. 收集覆盖率数据
lcov --capture --directory build --output-file coverage.info
lcov --remove coverage.info '/usr/*' '*/test/*' --output-file coverage_filtered.info
# 4. 生成 HTML 报告
genhtml coverage_filtered.info --output-directory coverage_report
# 5. 打开报告
xdg-open coverage_report/index.html
10.7.3 CTest 覆盖率
ctest -T Coverage
10.8 内存检查(MemCheck)
10.8.1 Valgrind 集成
# CTest 自动使用 Valgrind
# CTestCustom.cmake
set(CTEST_MEMORYCHECK_COMMAND "valgrind")
set(CTEST_MEMORYCHECK_COMMAND_OPTIONS
"--leak-check=full --show-reachable=yes --track-origins=yes"
)
# 运行内存检查
ctest -T MemCheck
ctest -T MemCheck --output-on-failure
10.8.2 AddressSanitizer
option(ENABLE_ASAN "启用 AddressSanitizer" OFF)
if(ENABLE_ASAN)
add_compile_options(-fsanitize=address -fno-omit-frame-pointer)
add_link_options(-fsanitize=address)
endif()
cmake -S . -B build -DENABLE_ASAN=ON -DCMAKE_BUILD_TYPE=Debug
cmake --build build
ctest --test-dir build --output-on-failure
10.8.3 Sanitizer 汇总
| Sanitizer | 编译标志 | 用途 |
|---|---|---|
| AddressSanitizer (ASan) | -fsanitize=address | 内存错误、缓冲区溢出 |
| ThreadSanitizer (TSan) | -fsanitize=thread | 数据竞争、死锁 |
| MemorySanitizer (MSan) | -fsanitize=memory | 未初始化内存读取 |
| UndefinedBehavior (UBSan) | -fsanitize=undefined | 未定义行为 |
| LeakSanitizer (LSan) | -fsanitize=leak | 内存泄漏 |
10.9 测试夹具(Fixtures)
10.9.1 Setup 和 Cleanup
# 设置阶段:创建数据库
add_test(NAME SetupDB COMMAND setup_db)
set_tests_properties(SetupDB PROPERTIES
FIXTURES_SETUP database
)
# 运行阶段:依赖数据库
add_test(NAME TestQuery COMMAND test_query)
set_tests_properties(TestQuery PROPERTIES
FIXTURES_REQUIRED database
)
# 清理阶段:删除数据库
add_test(NAME CleanupDB COMMAND cleanup_db)
set_tests_properties(CleanupDB PROPERTIES
FIXTURES_CLEANUP database
)
10.9.2 Fixture 流程
SetupDB (FIXTURES_SETUP)
↓ 创建数据库
TestQuery (FIXTURES_REQUIRED database)
↓ 运行查询测试
CleanupDB (FIXTURES_CLEANUP)
↓ 删除数据库
10.10 CDash 集成
10.10.1 基本配置
include(CTest)
# CDash 服务器配置
set(CTEST_PROJECT_NAME "MyProject")
set(CTEST_NIGHTLY_START_TIME "00:00:00 UTC")
set(CTEST_DROP_METHOD "https")
set(CTEST_DROP_SITE "cdash.example.com")
set(CTEST_DROP_LOCATION "/submit.php?project=MyProject")
set(CTEST_CDASH_VERSION "3.0")
10.10.2 CTest 脚本
# ctest_run.cmake
cmake_minimum_required(VERSION 3.16)
set(CTEST_PROJECT_NAME "MyProject")
set(CTEST_SOURCE_DIRECTORY "/path/to/source")
set(CTEST_BINARY_DIRECTORY "/path/to/build")
set(CTEST_CMAKE_GENERATOR "Ninja")
ctest_start("Experimental")
ctest_configure(OPTIONS "-DCMAKE_BUILD_TYPE=Debug")
ctest_build()
ctest_test()
ctest_coverage()
ctest_submit()
ctest -S ctest_run.cmake
10.11 业务场景
场景:完整的测试工作流
# 项目顶层 CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(MyProject VERSION 1.0.0)
option(BUILD_TESTING "构建测试" ON)
option(ENABLE_COVERAGE "启用覆盖率" OFF)
option(ENABLE_ASAN "启用 ASan" OFF)
# 编译选项
if(ENABLE_ASAN)
add_compile_options(-fsanitize=address -fno-omit-frame-pointer)
add_link_options(-fsanitize=address)
endif()
if(ENABLE_COVERAGE)
add_compile_options(--coverage)
add_link_options(--coverage)
endif()
add_subdirectory(src)
if(BUILD_TESTING)
enable_testing()
add_subdirectory(tests)
endif()
10.12 注意事项
| 问题 | 说明 |
|---|---|
| 忘记 enable_testing() | 测试不会注册到 CTest |
| 测试名重复 | 每个测试名必须唯一 |
| 工作目录问题 | 测试找不到数据文件时检查 WORKING_DIRECTORY |
| 并行测试资源冲突 | 使用 RESOURCE_LOCK 或 RUN_SERIAL |
| 覆盖率影响性能 | 仅在需要时启用 |
10.13 扩展阅读
上一章:第 9 章 — 工具链与交叉编译 | 下一章:第 11 章 — 安装与打包 →