21、Catch2高级特性:SECTION、GENERATOR、BENCHMARK宏、标签系统
Catch2 这个测试框架,我用了好几年了。说实话,刚开始接触时只觉得它比 Google Test 写起来更「现代」一些。但真正让我觉得它「香」的,是它的高级特性。今天我们就来聊聊 SECTION、GENERATOR、BENCHMARK 和标签系统。这几个东西,用好了能让你的测试代码量减少一半,覆盖率反而更高。
21.1 SECTION:测试用例内的「分叉」机制
先说说 SECTION。这是 Catch2 最让我惊艳的设计之一。你想想看,传统测试框架里,如果你想测试一个函数在不同输入下的行为,通常要写多个 TEST_CASE。但 Catch2 不一样,它允许你在一个 TEST_CASE 内部「分叉」出多个场景。
核心思想:每个 SECTION 共享 TEST_CASE 顶部的初始化代码,但各自独立执行后续逻辑。
TEST_CASE("用户登录测试") {
// 共享初始化
User user("test_user", "password123");
LoginService service;
SECTION("正确密码登录") {
auto result = service.login(user, "password123");
REQUIRE(result.success == true);
}
SECTION("错误密码登录") {
auto result = service.login(user, "wrong_password");
REQUIRE(result.success == false);
}
SECTION("空密码登录") {
auto result = service.login(user, "");
REQUIRE(result.error_code == ErrorCode::EMPTY_PASSWORD);
}
}
这段代码执行时,Catch2 会为每个 SECTION 重新进入 TEST_CASE,执行顶部的初始化代码,然后只运行当前 SECTION 内的断言。我在项目中遇到过一种情况:有个模块的初始化特别耗时,如果写成三个独立的 TEST_CASE,每次都要重新初始化,测试跑起来慢得让人抓狂。用了 SECTION 之后,初始化只写一次,测试速度提升明显。
小技巧:SECTION 可以嵌套。内层 SECTION 会继承外层 SECTION 的上下文。但别嵌套太深,超过三层代码可读性就下降了。
21.2 GENERATOR:用数据驱动测试
GENERATOR 是 Catch2 的另一个杀手锏。说白了,它让你用「数据驱动」的方式写测试。你只需要描述数据怎么生成,Catch2 自动帮你遍历所有组合。
TEST_CASE("字符串反转测试") {
auto input = GENERATE("hello", "world", "Catch2", "a", "");
auto expected = GENERATE("olleh", "dlrow", "2htcaC", "a", "");
auto result = reverse_string(input);
REQUIRE(result == expected);
}
这段代码会生成 5 个测试用例,每个用例对应一组 input 和 expected。注意,GENERATE 是按「列」匹配的,不是笛卡尔积。如果你想做笛卡尔积,需要用 GENERATE_COPY 或 GENERATE_REF 配合 filter 来实现。
我个人习惯把 GENERATOR 和 SECTION 结合起来用。比如这样:
TEST_CASE("排序算法测试") {
auto data = GENERATE(
std::vector<int>{3, 1, 2},
std::vector<int>{5, 5, 5, 5},
std::vector<int>{1},
std::vector<int>{}
);
SECTION("冒泡排序") {
auto result = bubble_sort(data);
REQUIRE(std::is_sorted(result.begin(), result.end()));
}
SECTION("快速排序") {
auto result = quick_sort(data);
REQUIRE(std::is_sorted(result.begin(), result.end()));
}
}
你看,这样写,4 组数据 × 2 种排序算法 = 8 个测试用例,代码却只有十几行。我曾经用这个模式重构过一个老项目的测试,测试文件从 2000 行缩减到了 400 行,覆盖率反而从 60% 提升到了 85%。
注意:GENERATOR 生成的数据量如果太大,测试执行时间会暴涨。我建议每个 GENERATE 的数据量控制在 10 组以内。如果需要大量数据,考虑用 GENERATE_RANGE 或自定义生成器。
21.3 BENCHMARK:性能测试的「快照」工具
性能测试这块,Catch2 的 BENCHMARK 宏做得相当轻量。它不像 Google Benchmark 那样功能丰富,但胜在简单、够用。
TEST_CASE("容器性能对比") {
std::vector<int> vec(10000);
std::list<int> lst(10000);
std::iota(vec.begin(), vec.end(), 0);
std::iota(lst.begin(), lst.end(), 0);
BENCHMARK("vector 遍历") {
for (auto& v : vec) {
v += 1;
}
};
BENCHMARK("list 遍历") {
for (auto& l : lst) {
l += 1;
}
};
}
BENCHMARK 宏会自动运行多次,计算平均耗时和标准差。输出结果类似这样:
------------------------------------------------------------------
benchmark name samples iterations estimated
mean low mean high mean
std dev low std dev high std dev
------------------------------------------------------------------
vector 遍历 100 100 0.0001s
0.000012s 0.000011s 0.000013s
0.000001s 0.000001s 0.000002s
list 遍历 100 100 0.0003s
0.000045s 0.000043s 0.000047s
0.000003s 0.000002s 0.000004s
嗯,这里要注意:BENCHMARK 的结果受系统负载影响很大。我建议在 CI 环境中跑性能测试时,尽量让机器处于空闲状态。另外,BENCHMARK 内部不要包含断言,否则会污染性能数据。
我的经验:BENCHMARK 最适合做「回归性能测试」。每次代码提交后,对比本次和上次的性能数据,如果某个 benchmark 耗时增加了 20% 以上,就该查查是不是引入了性能退化。
21.4 标签系统:给测试用例「打标签」
标签系统是 Catch2 组织测试用例的利器。你可以在 TEST_CASE 后面用 [tag] 的形式添加标签,然后通过命令行选择性地运行。
TEST_CASE("用户注册测试", "[user][registration][smoke]") {
// ...
}
TEST_CASE("用户登录测试", "[user][login][smoke]") {
// ...
}
TEST_CASE("数据库连接测试", "[database][integration]") {
// ...
}
运行测试时,你可以这样筛选:
# 只运行 smoke 标签的测试
./test_runner [smoke]
# 运行 user 标签但不包含 integration 标签的测试
./test_runner [user]~[integration]
# 运行 user 或 database 标签的测试
./test_runner [user] or [database]
标签的命名我建议遵循团队约定。我们团队的习惯是:
| 标签类别 | 示例 | 说明 |
|---|---|---|
| 模块名 | [user], [order], [payment] | 标识所属模块 |
| 测试级别 | [unit], [integration], [e2e] | 标识测试范围 |
| 运行频率 | [smoke], [regression], [nightly] | 标识何时运行 |
| 特殊标记 | [slow], [flaky], [wip] | 标识测试特性 |
我曾经接手过一个项目,测试用例有 3000 多个,每次全量跑要 40 分钟。后来我给每个用例打了标签,CI 流程改成:提交代码时只跑 [smoke] 和 [unit],每天凌晨跑 [integration] 和 [nightly]。结果 CI 从 40 分钟降到了 5 分钟,开发效率提升了一大截。
避坑指南:我曾经犯过一个错误——给测试用例打了太多标签,导致标签之间逻辑混乱。比如一个用例同时打了 [slow] 和 [smoke],这明显不合理。建议每个用例的标签不超过 3 个,且语义上不能冲突。
21.5 知识体系总览
为了让你更直观地理解这些高级特性之间的关系,我画了一张图:
这四个特性,SECTION 解决的是「测试结构」问题,GENERATOR 解决的是「测试数据」问题,BENCHMARK 解决的是「性能验证」问题,标签系统解决的是「测试组织」问题。把它们组合起来用,你就能写出既简洁又全面的测试套件。
好了,这一章的内容就到这里。记住,工具再好,也要看你怎么用。我建议你从自己的项目里挑一个模块,试着用 SECTION 和 GENERATOR 重构一下测试,感受一下那种「代码量减半、覆盖率翻倍」的快感。
公众号:蓝海资料掘金营,微信deep3321