第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的核心脉络:
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