跳到主要内容

CMake CTest 测试

CTest 是 CMake 自带的测试工具,它与 CMake 构建系统紧密集成,可以方便地管理、执行和报告测试结果。本章将详细介绍如何使用 CTest 进行自动化测试。

CTest 简介

什么是 CTest?

CTest 是 CMake 套件的一部分,专门用于:

  1. 测试管理:定义和组织测试用例
  2. 测试执行:运行测试并收集结果
  3. 结果报告:生成测试报告和统计信息
  4. 持续集成:与 CDash 等仪表板系统集成

为什么使用 CTest?

  • 原生集成:与 CMake 无缝配合
  • 跨平台:支持 Windows、Linux、macOS
  • 灵活性:支持多种测试框架
  • 并行执行:自动并行运行测试
  • 丰富功能:标签、依赖、超时、重试等

基本使用

启用测试

CMakeLists.txt 中启用测试功能:

cmake_minimum_required(VERSION 3.15)
project(MyProject)

# 启用测试
enable_testing()

# 或者使用更现代的方式
include(CTest)

两种方式的区别

命令效果
enable_testing()仅启用 add_test() 命令
include(CTest)启用测试并添加更多功能(如内存检查)

添加简单测试

使用 add_test() 添加测试:

# 创建可执行文件
add_executable(myapp src/main.cpp)
add_executable(test_myapp tests/test_main.cpp)

# 添加测试
add_test(NAME MyTest COMMAND test_myapp)

运行测试

# 进入构建目录
cd build

# 运行所有测试
ctest

# 详细输出
ctest -V

# 更详细的输出
ctest -VV

测试定义详解

add_test 语法

add_test(NAME <name> COMMAND <command> [<arg>...]
[CONFIGURATIONS <config>...]
[WORKING_DIRECTORY <dir>]
[COMMAND_EXPAND_LISTS])

参数说明

NAME

测试的名称,用于识别和筛选:

add_test(NAME Unit.Math COMMAND math_tests)
add_test(NAME Unit.String COMMAND string_tests)
add_test(NAME Integration.API COMMAND api_tests)

COMMAND

要执行的命令,可以是可执行文件或内置命令:

# 执行可执行文件
add_test(NAME MyTest COMMAND my_test_exe)

# 执行 Python 脚本
add_test(NAME PyTest COMMAND python3 ${CMAKE_SOURCE_DIR}/tests/test.py)

# 使用生成器表达式
add_test(NAME MyTest COMMAND $<TARGET_FILE:my_test_exe>)

CONFIGURATIONS

限制测试在特定构建配置下运行:

add_test(NAME DebugTest COMMAND debug_test
CONFIGURATIONS Debug
)

add_test(NAME ReleaseTest COMMAND release_test
CONFIGURATIONS Release
)

WORKING_DIRECTORY

设置测试的工作目录:

add_test(NAME FileTest COMMAND file_test
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/test_data
)

使用目标作为测试命令

推荐使用目标名称而非文件路径:

# 推荐:使用目标名称
add_test(NAME MyTest COMMAND my_test_target)

# 不推荐:使用文件路径
add_test(NAME MyTest COMMAND ${CMAKE_BINARY_DIR}/my_test_exe)

原因:目标名称在不同生成器下都能正确工作,包括多配置生成器(如 Visual Studio)。

测试属性

使用 set_tests_properties() 设置测试属性:

超时设置

add_test(NAME LongTest COMMAND long_running_test)
set_tests_properties(LongTest PROPERTIES
TIMEOUT 300 # 5 分钟超时
)

超时后测试会被终止并标记为失败。

标签

为测试添加标签,便于分组执行:

add_test(NAME UnitTest1 COMMAND test1)
set_tests_properties(UnitTest1 PROPERTIES
LABELS "unit;quick"
)

add_test(NAME IntegrationTest COMMAND integration_test)
set_tests_properties(IntegrationTest PROPERTIES
LABELS "integration;slow"
)

依赖关系

定义测试之间的依赖:

add_test(NAME Setup COMMAND setup_script)
add_test(NAME DataTest COMMAND data_test)
set_tests_properties(DataTest PROPERTIES
DEPENDS Setup
)

依赖测试会按顺序执行,如果前置测试失败,后续测试会被跳过。

环境变量

为测试设置环境变量:

add_test(NAME EnvTest COMMAND env_test)
set_tests_properties(EnvTest PROPERTIES
ENVIRONMENT "MY_VAR=value;PATH=/custom/path:$ENV{PATH}"
)

跳过测试

根据条件跳过测试:

add_test(NAME LinuxOnlyTest COMMAND linux_test)
set_tests_properties(LinuxOnlyTest PROPERTIES
SKIP_REGULAR_EXPRESSION "SKIP"
)

if(NOT UNIX)
set_tests_properties(LinuxOnlyTest PROPERTIES
DISABLED TRUE
)
endif()

测试输出验证

通过检查输出来判断测试结果:

add_test(NAME OutputTest COMMAND output_test)
set_tests_properties(OutputTest PROPERTIES
PASS_REGULAR_EXPRESSION "All tests passed"
FAIL_REGULAR_EXPRESSION "ERROR;FAILED"
SKIP_REGULAR_EXPRESSION "SKIP"
)

使用 Google Test

集成 Google Test

使用 FetchContent 下载 Google Test:

cmake_minimum_required(VERSION 3.15)
project(MyProject)

include(FetchContent)
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.14.0
)
FetchContent_MakeAvailable(googletest)

enable_testing()

add_executable(my_tests
tests/test_main.cpp
tests/test_math.cpp
)

target_link_libraries(my_tests PRIVATE GTest::gtest_main)

# 自动注册所有测试
include(GoogleTest)
gtest_discover_tests(my_tests)

Google Test 示例代码

tests/test_math.cpp

#include <gtest/gtest.h>

// 被测试的函数
int add(int a, int b) {
return a + b;
}

// 测试用例
TEST(MathTest, AddPositiveNumbers) {
EXPECT_EQ(add(2, 3), 5);
}

TEST(MathTest, AddNegativeNumbers) {
EXPECT_EQ(add(-1, -1), -2);
}

TEST(MathTest, AddZero) {
EXPECT_EQ(add(0, 5), 5);
}

gtest_discover_tests 选项

gtest_discover_tests(my_tests
# 测试名称前缀
PROPERTIES LABELS "unit"
# 超时设置
TIMEOUT 60
# 额外参数
EXTRA_ARGS --gtest_output=xml:test_results/
# 测试列表超时
DISCOVERY_TIMEOUT 30
)

使用 Catch2

集成 Catch2

include(FetchContent)
FetchContent_Declare(
Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.5.0
)
FetchContent_MakeAvailable(Catch2)

enable_testing()

add_executable(my_tests tests/test_main.cpp)
target_link_libraries(my_tests PRIVATE Catch2::Catch2WithMain)

# 自动注册测试
include(Catch)
catch_discover_tests(my_tests)

Catch2 示例代码

#include <catch2/catch_test_macros.hpp>

TEST_CASE("Addition works correctly", "[math]") {
REQUIRE(1 + 1 == 2);
REQUIRE(2 + 2 == 4);
}

TEST_CASE("Division by zero throws", "[math]") {
REQUIRE_THROWS(10 / 0);
}

SECTION("Nested sections") {
SECTION("First section") {
REQUIRE(true);
}
SECTION("Second section") {
REQUIRE(true);
}
}

运行测试

基本命令

# 运行所有测试
ctest

# 指定构建目录
ctest --test-dir build

# 显示进度
ctest --progress

详细输出

# 详细输出
ctest -V

# 更详细的输出
ctest -VV

# 仅失败时输出
ctest --output-on-failure

# 失败时停止
ctest --stop-on-failure

筛选测试

# 按名称匹配
ctest -R MathTest # 正则匹配测试名
ctest -R "Unit.*" # 正则表达式

# 排除测试
ctest -E Integration # 排除名称包含 Integration 的测试

# 按标签筛选
ctest -L unit # 运行标签为 unit 的测试
ctest -LE slow # 排除标签为 slow 的测试

# 多个标签(AND 关系)
ctest -L unit -L quick

并行执行

# 并行运行(自动检测 CPU 核心数)
ctest -j

# 指定并行数
ctest -j4

# 使用环境变量
CTEST_PARALLEL_LEVEL=4 ctest

构建配置

对于多配置生成器(如 Visual Studio):

# 指定配置
ctest -C Debug
ctest -C Release

重试失败的测试

# 重新运行失败的测试
ctest --rerun-failed

# 重复运行直到通过
ctest --repeat until-pass:5

# 重复运行直到失败(用于检测不稳定测试)
ctest --repeat until-fail:10

高级功能

测试固件(Fixtures)

测试固件用于管理测试的设置和清理:

# 设置固件
add_test(NAME SetupDB COMMAND setup_database)
set_tests_properties(SetupDB PROPERTIES
FIXTURES_SETUP DB
)

# 清理固件
add_test(NAME CleanupDB COMMAND cleanup_database)
set_tests_properties(CleanupDB PROPERTIES
FIXTURES_CLEANUP DB
)

# 使用固件的测试
add_test(NAME DBTest1 COMMAND db_test1)
set_tests_properties(DBTest1 PROPERTIES
FIXTURES_REQUIRED DB
)

固件执行顺序

  1. SetupDB
  2. DBTest1
  3. CleanupDB

如果 SetupDB 失败,DBTest1 会被跳过,CleanupDB 仍会执行。

资源分配

限制测试使用的资源:

add_test(NAME GPUTest COMMAND gpu_test)
set_tests_properties(GPUTest PROPERTIES
RESOURCE_LOCK gpu
)

add_test(NAME AnotherGPUTest COMMAND another_gpu_test)
set_tests_properties(AnotherGPUTest PROPERTIES
RESOURCE_LOCK gpu
)

使用相同资源锁的测试不会并行执行。

自定义测试命令

编写自定义测试脚本:

tests/run_test.py

#!/usr/bin/env python3
import subprocess
import sys

# 运行程序
result = subprocess.run(['./myapp', '--test'], capture_output=True)

# 检查输出
if b'All tests passed' in result.stdout:
sys.exit(0)
else:
print("Test failed:", result.stdout.decode())
sys.exit(1)

CMakeLists.txt

find_package(Python3 REQUIRED)

add_test(NAME CustomTest
COMMAND Python3::Interpreter ${CMAKE_SOURCE_DIR}/tests/run_test.py
)

测试报告

JUnit 格式输出

# 生成 JUnit XML 报告
ctest --output-junit test_results.xml

输出到文件

# 输出日志
ctest -O test_log.txt

# 仅失败时输出到文件
ctest --output-on-failure -O failures.txt

配置测试输出大小

# 限制通过测试的输出大小
set(CMAKE_CTEST_OUTPUT_ON_FAILURE ON)

# 命令行控制
ctest --test-output-size-passed 1024 # 1KB
ctest --test-output-size-failed 10240 # 10KB

CDash 集成

CTest 可以将测试结果提交到 CDash 仪表板:

配置 CTest

CTestConfig.cmake

set(CTEST_PROJECT_NAME "MyProject")
set(CTEST_NIGHTLY_START_TIME "00:00:00 UTC")

set(CTEST_DROP_METHOD "http")
set(CTEST_DROP_SITE "my.cdash.org")
set(CTEST_DROP_LOCATION "/submit.php?project=MyProject")
set(CTEST_DROP_SITE_CDASH TRUE)

提交测试结果

# 实验性提交
ctest -D Experimental

# 每日构建
ctest -D Nightly

# 持续集成
ctest -D Continuous

最佳实践

1. 测试命名规范

# 推荐的命名模式:模块.功能.场景
add_test(NAME Calculator.Add.PositiveNumbers COMMAND ...)
add_test(NAME Calculator.Add.NegativeNumbers COMMAND ...)
add_test(NAME Calculator.Divide.ByZero COMMAND ...)

2. 合理使用标签

set_tests_properties(QuickTest PROPERTIES LABELS "quick;unit")
set_tests_properties(SlowTest PROPERTIES LABELS "slow;integration")
set_tests_properties(NightlyTest PROPERTIES LABELS "nightly")
# 开发时运行快速测试
ctest -L quick

# CI 中运行所有单元测试
ctest -L unit

# 夜间构建运行所有测试
ctest

3. 设置合理的超时

# 单元测试:快速超时
set_tests_properties(UnitTest PROPERTIES TIMEOUT 10)

# 集成测试:较长超时
set_tests_properties(IntegrationTest PROPERTIES TIMEOUT 300)

# 性能测试:允许更长时间
set_tests_properties(PerformanceTest PROPERTIES TIMEOUT 3600)

4. 测试分组

# 使用标签分组
add_test(NAME Unit.Math COMMAND ...)
set_tests_properties(Unit.Math PROPERTIES LABELS "unit;math")

add_test(NAME Unit.String COMMAND ...)
set_tests_properties(Unit.String PROPERTIES LABELS "unit;string")

add_test(NAME Integration.API COMMAND ...)
set_tests_properties(Integration.API PROPERTIES LABELS "integration;api")

5. 条件测试

# 平台特定测试
if(WIN32)
add_test(NAME WindowsTest COMMAND windows_test)
elseif(UNIX)
add_test(NAME LinuxTest COMMAND linux_test)
endif()

# 功能条件测试
if(ENABLE_GPU_TESTS)
add_test(NAME GPUTest COMMAND gpu_test)
endif()

命令参考

常用 ctest 命令选项

选项说明
-R <regex>运行匹配正则的测试
-E <regex>排除匹配正则的测试
-L <label>运行指定标签的测试
-LE <label>排除指定标签的测试
-C <config>指定构建配置
-j <n>并行运行测试
-V详细输出
--output-on-failure失败时显示输出
--stop-on-failure失败时停止
--rerun-failed重运行失败的测试
--timeout <sec>设置全局超时
-N显示测试列表但不运行

小结

本章我们学习了:

  1. CTest 基础:enable_testing、add_test
  2. 测试属性:超时、标签、依赖、环境变量
  3. 测试框架集成:Google Test、Catch2
  4. 运行测试:筛选、并行、重试
  5. 高级功能:固件、资源锁、自定义脚本
  6. 测试报告:JUnit 格式、CDash 集成

CTest 是 CMake 项目的标准测试工具,掌握它能有效提升项目的质量保证能力。

练习

  1. 为一个计算器模块编写单元测试
  2. 使用 Google Test 或 Catch2 创建测试项目
  3. 配置测试标签,区分快速测试和慢速测试
  4. 设置测试超时和重试机制

参考资源