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,而不是 mastermain。为什么?因为主分支随时可能 break,你不想某天 CI 突然挂了,只因为 Catch2 更新了一个不兼容的提交。
  • FetchContent_MakeAvailable 会帮你把 Catch2 的 CMake 目标暴露出来,后面直接链接 Catch2::Catch2Catch2::Catch2WithMain 就行。
小提示:如果你网络不好,或者公司内网不能访问 GitHub,可以先把 Catch2 的源码包放到内部 Git 服务器上,把 GIT_REPOSITORY 改成内网地址即可。我之前的项目就是这么干的,省去了很多麻烦。

第二步:理解 CATCH_CONFIG_MAIN

好,现在 Catch2 已经下载到你的构建目录里了。接下来要写测试文件。这里有一个新手容易踩的坑——main 函数谁来写?

Catch2 提供了两种模式:

  1. 自带 main:你只需要在 一个 .cpp 文件里定义 CATCH_CONFIG_MAIN,Catch2 会自动生成 main 函数,帮你解析命令行参数、运行所有测试用例。
  2. 自定义 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::Catch2WithMainCatch2::Catch2 有什么区别?

目标名称 说明 适用场景
Catch2::Catch2 只提供测试宏和断言,不提供 main 函数 你需要自定义 main 函数,或者已经在某个 .cpp 里定义了 CATCH_CONFIG_MAIN
Catch2::Catch2WithMain 包含一个默认的 main 函数 你不想写 test_main.cpp,直接链接即可

我个人习惯用 Catch2::Catch2 加上一个显式的 test_main.cpp。为什么?因为有时候我需要自定义 main 函数,比如初始化日志系统、加载配置文件。如果用了 Catch2WithMain,这些初始化逻辑就没地方放了。

整体流程回顾

说了这么多,我们来画一张图,把整个流程串起来。你想想看,从 CMake 配置到运行测试,其实就三步:

Catch2 集成流程 1. FetchContent 下载 CMakeLists.txt 声明依赖 2. 编写测试用例 TEST_CASE + REQUIRE 3. CTest 运行 add_test + ctest 关键文件结构 项目根目录/ ├── CMakeLists.txt # 主配置,FetchContent 下载 Catch2 ├── src/ # 你的项目源码 ├── tests/ ├── CMakeLists.txt # 生成 test_runner,注册 CTest ├── test_main.cpp # 定义 CATCH_CONFIG_MAIN └── test_math.cpp # 具体的 TEST_CASE

完整示例:跑起来看看

最后,我把所有文件拼在一起,给你一个可以直接复制粘贴的完整示例。你只需要新建一个目录,把文件放进去,然后:

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