第62章:Catch2:集成Catch2测试框架

说实话,很多C++开发者对单元测试的态度是「知道很重要,但就是懒得写」。我以前也这样,觉得测试代码比业务代码还难写,直到有一次重构一个核心模块,改完直接崩了,排查了两天才发现是某个边界条件没处理好。嗯,从那以后,我再也不敢不写测试了。

这一章,我带你搞定Catch2——目前C++社区里最流行的测试框架之一。它轻量、头文件即可使用,而且语法非常友好。说白了,你只要会写C++函数,就能写Catch2测试。

62.1 Catch2是什么?

Catch2是一个纯头文件的C++测试框架。它的全称是「C++ Automated Test Cases in Headers」,版本2.x是单头文件版本,3.x开始拆成了多个模块。我个人更推荐2.x版本,集成起来特别省事。

它的核心特点:

  • 头文件即可用:不需要编译库,直接#include一个头文件
  • BDD风格支持:可以用Given-When-Then写测试
  • 自动注册测试用例:你只管写TEST_CASE,框架自动收集
  • 丰富的断言宏:REQUIRE、CHECK、REQUIRE_THROWS等
  • 支持Section:一个测试用例可以拆成多个子场景

核心优势:Catch2的测试用例是自描述的。你不需要额外写main函数,也不需要手动注册。写一个TEST_CASE,它自动就变成可执行的测试项了。

62.2 在CMake中集成Catch2

集成方式有两种:一种是直接把头文件下载到项目里,另一种是用FetchContent自动拉取。我建议用FetchContent,这样版本管理更清晰。

方式一:手动下载头文件

# 下载 catch.hpp 放到项目目录
# 在CMakeLists.txt中直接使用
add_executable(my_test test_main.cpp)
target_include_directories(my_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/catch2)

方式二:使用FetchContent(推荐)

include(FetchContent)

FetchContent_Declare(
  Catch2
  GIT_REPOSITORY https://github.com/catchorg/Catch2.git
  GIT_TAG        v2.13.10
)

FetchContent_MakeAvailable(Catch2)

# 这样Catch2::Catch2这个target就可用啦
target_link_libraries(my_test PRIVATE Catch2::Catch2)

我在项目中遇到过一个问题:如果同时用Catch2和Google Test,两个框架的宏可能会有冲突。解决办法是给Catch2加上命名空间前缀,或者干脆只用一个框架。我个人习惯是统一用Catch2,省心。

62.3 编写第一个测试用例

来看一个最简单的例子。假设我们有一个加法函数:

// math.hpp
int add(int a, int b) {
    return a + b;
}

对应的测试代码:

// test_math.cpp
#define CATCH_CONFIG_MAIN  // 这个宏告诉Catch2生成main函数
#include "catch.hpp"
#include "math.hpp"

TEST_CASE("加法测试", "[math]") {
    REQUIRE(add(1, 2) == 3);
    REQUIRE(add(-1, 1) == 0);
    REQUIRE(add(0, 0) == 0);
}

编译运行后,你会看到类似这样的输出:

All tests passed (3 assertions in 1 test case)

如果某个断言失败,Catch2会精确告诉你哪一行、期望值是多少、实际值是多少。这个调试体验比你自己写cout强太多了。

62.4 测试用例的组织结构

Catch2支持用Section来组织同一个测试用例下的不同场景。举个例子:

TEST_CASE("字符串处理测试", "[string]") {
    std::string str = "Hello, Catch2!";

    SECTION("检查长度") {
        REQUIRE(str.length() == 15);
    }

    SECTION("检查子串") {
        REQUIRE(str.find("Catch2") != std::string::npos);
    }

    SECTION("检查空字符串") {
        str.clear();
        REQUIRE(str.empty());
    }
}

每个SECTION都是独立的。Catch2会为每个SECTION重新执行一次TEST_CASE的初始化代码。这意味着你不用担心一个SECTION修改了状态影响另一个SECTION。

小技巧:SECTION可以嵌套。你可以在一个SECTION里再写SECTION,形成树状结构。这在测试复杂业务逻辑时特别有用。

62.5 常用的断言宏

Catch2提供了两套断言体系:

说明 失败时行为
REQUIRE 必须满足,否则终止当前测试用例 立即失败
CHECK 检查条件,失败后继续执行 记录失败,继续运行
REQUIRE_FALSE 要求表达式为false 立即失败
REQUIRE_THROWS 要求抛出异常 未抛出则失败
REQUIRE_NOTHROW 要求不抛出异常 抛出则失败

我曾经踩过一个坑:在循环里用REQUIRE,结果第一个元素失败后整个测试就停了,后面的元素都没测到。后来改成CHECK,至少能跑完一轮,把所有失败点都暴露出来。

62.6 测试夹具(Fixture)

当多个测试用例需要相同的初始化代码时,可以用TEST_CASE_METHOD:

struct MyFixture {
    MyFixture() : data(42) {
        // 初始化代码
    }
    int data;
};

TEST_CASE_METHOD(MyFixture, "使用夹具测试", "[fixture]") {
    REQUIRE(data == 42);
}

或者更优雅的方式——直接用SECTION,Catch2会自动处理生命周期。我个人更倾向于SECTION,因为代码更紧凑,阅读起来更流畅。

62.7 BDD风格测试

Catch2支持BDD(行为驱动开发)风格的测试,用SCENARIO、GIVEN、WHEN、THEN来组织:

SCENARIO("用户登录", "[login]") {
    GIVEN("一个已注册的用户") {
        // 准备数据
        WHEN("输入正确的密码") {
            // 执行操作
            THEN("登录成功") {
                REQUIRE(true);
            }
        }
        WHEN("输入错误的密码") {
            THEN("登录失败") {
                REQUIRE(true);
            }
        }
    }
}

这种写法特别适合业务逻辑测试。你想想看,产品经理给的需求文档里写的「当用户输入错误密码时,系统应提示错误」,直接就能映射成测试代码。测试即文档,文档即测试。

62.8 在CMake中配置测试运行

集成Catch2后,别忘了用CMake的enable_testing和add_test来注册测试:

enable_testing()

add_executable(test_math test_math.cpp)
target_link_libraries(test_math PRIVATE Catch2::Catch2)

add_test(NAME test_math COMMAND test_math)

然后你就可以用ctest来运行所有测试了:

mkdir build && cd build
cmake ..
cmake --build .
ctest --output-on-failure

--output-on-failure这个参数很关键。不加的话,测试失败只会告诉你「FAILED」,加了才能看到Catch2输出的详细信息。

62.9 知识体系总览

下面这张图帮你理清Catch2的核心脉络:

Catch2 测试框架 集成方式 测试组织 断言体系 FetchContent 手动头文件 TEST_CASE SECTION SCENARIO REQUIRE CHECK CMake + ctest 运行 测试报告:通过/失败详情

62.10 避坑指南

最后分享几个我踩过的坑:

  • 忘记定义CATCH_CONFIG_MAIN:如果你只有一个测试文件,记得在其中一个cpp里定义这个宏。否则链接时会报找不到main函数。
  • 头文件路径问题:用FetchContent时,Catch2的头文件路径会自动添加。但如果你手动下载,别忘了target_include_directories。
  • 测试文件太多:当测试文件超过10个时,建议每个文件单独编译成库,再链接到主测试程序。否则编译一次要等很久。
  • 浮点数比较:别直接用REQUIRE(a == b)比较浮点数。用REQUIRE(Approx(a) == b),Catch2会处理精度问题。

特别注意:Catch2 v3版本和v2版本在API上有较大差异。如果你在网上搜到的是v3的示例代码,直接复制到v2项目里可能会编译不过。建议统一版本,不要混用。

好了,这一章的内容就到这里。Catch2的集成其实不复杂,关键是养成写测试的习惯。你想想看,每次提交代码前跑一遍测试,心里踏实多了。下次重构的时候,你就知道这玩意儿有多香了。


公众号:蓝海资料掘金营,微信deep3321