需定义 CATCH_CONFIG_ENABLE_BDD_MACROS 宏(在 #include catch_test_macros.hpp 后、测试宏前),用 SCENARIO 描述场景、SECTION 命名模拟 GIVEN/WHEN/THEN,注意 SECTION 嵌套会重复执行外层代码,应避免副作用并显式重建上下文。

如何用 Catch2 单头文件启动 BDD 风格测试
Catch2 的 Catch2/single_include/catch2/catch_test_macros.hpp 确实支持 BDD 风格,但不是开箱即用的 GIVEN/WHEN/THEN 三段式——它靠 SCENARIO + SECTION 模拟,且必须手动启用 BDD 宏。直接包含单头文件后,默认只认 TEST_CASE。
要写 BDD 风格,得加一行预处理定义:
#define CATCH_CONFIG_RUNNER #include <catch2/catch_test_macros.hpp> <p>// 必须在 #include catch_test_macros.hpp 之后、任何测试宏之前定义</p><h1>define CATCH_CONFIG_ENABLE_BDD_MACROS
注意顺序:如果把 #define CATCH_CONFIG_ENABLE_BDD_MACROS 放在 #include 前,宏不会生效;放在某个 SCENARIO 后面,该文件后续测试也无效。
立即学习“C++免费学习笔记(深入)”;
SCENARIO 和 SECTION 怎么对应 GIVEN/WHEN/THEN
Catch2 的 BDD 支持是语义映射,不是语法重载:SCENARIO 对应整个用例描述,SECTION 才承担行为分段职责。它不强制“一个 SECTION = 一个关键字”,而是靠嵌套和命名模拟逻辑层次。
-
SCENARIO只能有一层,描述用户视角的完整业务场景,如"SCENARIO("用户登录失败时显示错误提示") -
SECTION可嵌套,推荐用GIVEN/WHEN/THEN作为 SECTION 名(纯字符串,无语法约束),例如:SECTION("GIVEN 未填写密码")→SECTION("WHEN 点击登录按钮")→SECTION("THEN 显示 '密码不能为空' 提示") - 每个
SECTION是独立执行路径,会重跑外层SCENARIO中其上方所有代码(包括变量声明),所以不要在 SECTION 外初始化易变状态
典型结构:
SCENARIO("用户登录失败时显示错误提示") {
// 这里放公共 fixture,比如创建 LoginController 实例
LoginController ctrl;
<pre class='brush:php;toolbar:false;'>SECTION("GIVEN 未填写密码") {
ctrl.set_username("testuser");
// 密码留空
SECTION("WHEN 点击登录按钮") {
auto result = ctrl.attempt_login();
SECTION("THEN 显示 '密码不能为空' 提示") {
REQUIRE(result.error_message == "密码不能为空");
}
}
}}
为什么 SECTION 嵌套容易出错?怎么避免
SECTION 嵌套看似直观,但 Catch2 实际按深度优先遍历执行:每个最内层 SECTION 都会重复执行所有外层代码。这意味着变量重复构造、资源反复申请、甚至断言被多次触发——尤其当 SECTION 内含副作用(如文件写入、网络调用)时,行为不可预测。
- 避免在 SECTION 外做有状态操作,比如
std::ofstream f("test.log");—— 每次进入新 SECTION 都会重新打开并覆盖文件 - 不要依赖 SECTION 顺序隐含的“状态流转”;BDD 描述是线性的,但执行是离散的。真需要状态传递,改用
static局部变量或提取到 lambda,但要小心多线程测试并发问题 - 更稳健的做法是:每个最内层 SECTION 都当作独立测试用例来写,显式重建必要上下文,哪怕略冗余。例如在每个 THEN SECTION 里都重新构造
ctrl并设好前置条件 - 调试时加
std::cout << "Running: " << Catch::getResultCapture().getCurrentTestName() << '\n';可看清实际执行流
性能和可维护性上要注意什么
单头文件模式下,Catch2 编译速度比模块化版本慢,尤其在大量 SCENARIO 文件中重复包含宏定义时。BDD 风格又天然倾向拆细 SECTION,进一步放大开销。
- 每个测试文件尽量控制在 1–3 个
SCENARIO,避免一个文件塞十几层 SECTION 嵌套 - 不用
SECTION模拟复杂状态机;真正需要多步骤验证的逻辑,拆成多个独立SCENARIO,用清晰命名体现关系,比如"Login failure - empty password"、"Login failure - wrong password" - 若项目已用 CMake,建议启用
catch2_WITH_MAIN并链接库版本,而非全单头;单头适合快速原型或 CI 临时脚本,长期维护项目中宏膨胀会导致编译失败难定位 - 注意
REQUIRE在 SECTION 内失败会终止当前 SECTION,但不影响其他 SECTION 执行——这和传统单元测试“一个断言失败就跳过后续”不同,得适应它的并行式失败报告逻辑
最常被忽略的一点:Catch2 的 BDD 宏只是标签糖,不提供 Given-When-Then 的 DSL 解析能力。所有逻辑仍靠 C++ 表达,别指望它自动推导前置条件或回滚状态。写的时候图省事用嵌套 SECTION,跑起来才发现 IO 冲突或内存泄漏,就得回头重组织——先想清楚哪些是真正共享 setup,哪些必须隔离,比选语法风格重要得多。


















