强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 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_SETUPFixture 设置"db_fixture"
FIXTURES_REQUIREDFixture 需求"db_fixture"
FIXTURES_CLEANUPFixture 清理"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 章 — 安装与打包 →