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_COPYGENERATE_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 知识体系总览

为了让你更直观地理解这些高级特性之间的关系,我画了一张图:

Catch2 高级特性 SECTION • 共享初始化代码 • 支持嵌套分叉 • 减少重复代码 GENERATOR • 数据驱动测试 • 自动遍历组合 • 与 SECTION 配合 BENCHMARK • 性能回归测试 • 自动多次运行 • 输出均值/标准差 标签系统 • 按标签筛选运行 • 支持逻辑组合 • 加速 CI 流程

这四个特性,SECTION 解决的是「测试结构」问题,GENERATOR 解决的是「测试数据」问题,BENCHMARK 解决的是「性能验证」问题,标签系统解决的是「测试组织」问题。把它们组合起来用,你就能写出既简洁又全面的测试套件。

好了,这一章的内容就到这里。记住,工具再好,也要看你怎么用。我建议你从自己的项目里挑一个模块,试着用 SECTION 和 GENERATOR 重构一下测试,感受一下那种「代码量减半、覆盖率翻倍」的快感。


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