20、Catch2集成:FetchContent引入Catch2、CATCH_CONFIG_MAIN、TEST_CASE宏
单元测试框架,说白了就是帮你验证「代码到底对不对」的工具。C++世界里选择不少,Google Test、Boost.Test、Catch2 各有拥趸。我个人这几年用得最多的反而是 Catch2 —— 原因很简单:它头文件只有几个,集成起来几乎零成本,而且宏定义写起来特别顺手。
今天我们就聊聊怎么把 Catch2 请进你的 CMake 项目里。嗯,这里要注意,我不会让你去手动下载源码包,那太原始了。我们用 CMake 自带的 FetchContent 模块,一键拉取、编译、链接,全自动搞定。
为什么选 FetchContent?
你可能会问:「我直接把 Catch2 的头文件扔到项目里不行吗?」行,当然行。但问题在于——版本管理。你扔进去的是 2.x 还是 3.x?团队其他人怎么同步?CI 环境里还要手动拷贝一遍?
我在项目中遇到过好几次这种尴尬:本地跑得好好的,一上 CI 就报找不到头文件。排查半天,原来是有人忘了把 Catch2 提交到 Git。从那以后,我坚决用 FetchContent 管理所有测试依赖。
它的好处很明显:
- 声明式依赖:在 CMakeLists.txt 里写清楚从哪下载、什么版本
- 自动缓存:第一次下载后,后续构建直接用缓存,不会反复拉取
- 与项目无缝集成:下载后的目标可以直接
target_link_libraries
第一步:用 FetchContent 引入 Catch2
先看一个最简的 CMakeLists.txt 配置。我习惯把测试相关的逻辑单独放在一个 CMakeLists.txt 里,或者至少用 if(BUILD_TESTING) 包裹起来,方便开关。
cmake_minimum_required(VERSION 3.14)
project(MyApp LANGUAGES CXX)
# 可选:让用户通过 -DBUILD_TESTING=OFF 关闭测试
option(BUILD_TESTING "Build tests" ON)
if(BUILD_TESTING)
include(FetchContent)
# 声明 Catch2 依赖
FetchContent_Declare(
Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.5.3
)
# 让 CMake 下载并构建
FetchContent_MakeAvailable(Catch2)
# 开启测试功能
enable_testing()
# 添加测试子目录
add_subdirectory(tests)
endif()
这里有几个细节我想强调一下:
cmake_minimum_required(VERSION 3.14)是必须的,因为FetchContent在 3.14 才正式稳定。低于这个版本会报错。GIT_TAG我建议写具体的 release 版本号,比如v3.5.3,而不是master或main。为什么?因为主分支随时可能 break,你不想某天 CI 突然挂了,只因为 Catch2 更新了一个不兼容的提交。FetchContent_MakeAvailable会帮你把 Catch2 的 CMake 目标暴露出来,后面直接链接Catch2::Catch2或Catch2::Catch2WithMain就行。
GIT_REPOSITORY 改成内网地址即可。我之前的项目就是这么干的,省去了很多麻烦。
第二步:理解 CATCH_CONFIG_MAIN
好,现在 Catch2 已经下载到你的构建目录里了。接下来要写测试文件。这里有一个新手容易踩的坑——main 函数谁来写?
Catch2 提供了两种模式:
- 自带 main:你只需要在 一个 .cpp 文件里定义
CATCH_CONFIG_MAIN,Catch2 会自动生成 main 函数,帮你解析命令行参数、运行所有测试用例。 - 自定义 main:你自己写 main 函数,然后调用
Catch::Session来手动控制测试流程。这种场景比较少见,一般用于需要初始化某些全局资源的情况。
我个人建议:99% 的项目用第一种就够了。简单、省事、不出错。
来看一个典型的测试文件结构:
// tests/test_main.cpp
// 这个文件只做一件事:生成 main 函数
#define CATCH_CONFIG_MAIN
#include <catch2/catch_all.hpp>
// tests/test_math.cpp
// 这里写具体的测试用例,不需要再定义 main
#include <catch2/catch_all.hpp>
#include "my_math.h"
TEST_CASE("加法测试", "[math]") {
REQUIRE(add(1, 2) == 3);
REQUIRE(add(-1, 1) == 0);
}
TEST_CASE("除法测试", "[math][edge]") {
REQUIRE(divide(10, 2) == 5);
REQUIRE_THROWS_AS(divide(1, 0), std::invalid_argument);
}
注意看:test_main.cpp 里定义了 CATCH_CONFIG_MAIN,而 test_math.cpp 里没有。如果你不小心在多个文件里都定义了 CATCH_CONFIG_MAIN,链接器会报「重复的 main 函数」错误。嗯,我刚开始用 Catch2 时就犯过这个错,排查了十分钟才反应过来。
CATCH_CONFIG_MAIN 只能出现在一个 .cpp 文件中。如果你用 Catch2 v3,也可以链接 Catch2::Catch2WithMain 目标,这样连 test_main.cpp 都不需要写了。我个人更倾向于显式写一个 test_main.cpp,因为这样更清晰,也方便以后扩展自定义 main。
第三步:TEST_CASE 宏的用法
TEST_CASE 是 Catch2 最核心的宏。它的语法很简单:
TEST_CASE("测试描述", "[标签1][标签2]") {
// 测试代码
REQUIRE(条件);
CHECK(条件);
// ...
}
第一个参数是测试用例的名称,最好写得清晰一点,比如「用户登录-密码正确时返回成功」。第二个参数是标签,用方括号包裹,可以写多个。标签的作用是让你在运行测试时做筛选:
# 只运行带有 [math] 标签的测试
./test_runner [math]
# 运行除了 [slow] 之外的所有测试
./test_runner ~[slow]
我在项目中习惯给每个模块分配一个标签,比如 [math]、[network]、[database],再给一些边界情况打上 [edge] 标签。这样 CI 里可以快速跑「冒烟测试」(只跑 [smoke] 标签),而 nightly build 再跑全量测试。
第四步:在 CMake 中注册测试
测试文件写好了,怎么让 CTest 知道它们的存在?很简单,用 add_test 注册:
# tests/CMakeLists.txt
# 生成测试可执行文件
add_executable(test_runner
test_main.cpp
test_math.cpp
test_network.cpp
)
# 链接 Catch2 和被测库
target_link_libraries(test_runner
PRIVATE
Catch2::Catch2WithMain # 如果你不想写 test_main.cpp,就用这个
my_library # 你的项目库
)
# 注册到 CTest
add_test(NAME test_runner COMMAND test_runner)
这里有一个选择:Catch2::Catch2WithMain 和 Catch2::Catch2 有什么区别?
| 目标名称 | 说明 | 适用场景 |
|---|---|---|
Catch2::Catch2 |
只提供测试宏和断言,不提供 main 函数 | 你需要自定义 main 函数,或者已经在某个 .cpp 里定义了 CATCH_CONFIG_MAIN |
Catch2::Catch2WithMain |
包含一个默认的 main 函数 | 你不想写 test_main.cpp,直接链接即可 |
我个人习惯用 Catch2::Catch2 加上一个显式的 test_main.cpp。为什么?因为有时候我需要自定义 main 函数,比如初始化日志系统、加载配置文件。如果用了 Catch2WithMain,这些初始化逻辑就没地方放了。
整体流程回顾
说了这么多,我们来画一张图,把整个流程串起来。你想想看,从 CMake 配置到运行测试,其实就三步:
完整示例:跑起来看看
最后,我把所有文件拼在一起,给你一个可以直接复制粘贴的完整示例。你只需要新建一个目录,把文件放进去,然后:
mkdir build && cd build
cmake ..
cmake --build .
ctest --output-on-failure
就能看到测试结果了。如果一切顺利,你会看到类似这样的输出:
Test project /path/to/build
Start 1: test_runner
1/1 Test #1: test_runner ...................... Passed 0.02 sec
100% tests passed, 0 tests failed out of 1
如果某个测试失败了,--output-on-failure 会把 Catch2 的详细断言信息打印出来,方便你定位问题。这个选项我每次都会加,省得再跑一遍看日志。
- 用
FetchContent管理 Catch2 依赖,版本锁定到具体 tag - 在 一个 .cpp 文件中定义
CATCH_CONFIG_MAIN,生成 main 函数 - 用
TEST_CASE宏编写测试用例,用REQUIRE/CHECK做断言 - 通过
add_test注册到 CTest,用ctest命令统一运行
好了,Catch2 的集成就聊到这里。这套流程我用了好几年,从个人项目到团队协作都没出过问题。你只要照着做,测试环境搭建这块基本不会踩坑。
公众号:蓝海资料掘金营,微信deep3321