从零玩转 Catch2:C++ 单元测试框架小白实战指南

发布时间:2026/8/8 21:16:22
从零玩转 Catch2:C++ 单元测试框架小白实战指南 面向零基础读者术语首次出现用通俗类比解释代码全部带中文注释关键易错处有 ⚠️ 预警文末附 FAQ 速查表。本文基于 Catch2 v3当前主线版本并会指出 v2 的差异。一、为什么我们需要单元测试先想一个场景你写了一个计算器类 Calculator能加能减能乘能除。代码写完你手动输入几个数字试了试感觉没问题就上线了。一周后同事改了个小数点处理结果除法全乱套——但你根本不知道是哪一步改坏的。单元测试Unit Testing就是给这段代码上的安全网把每一个函数/类的行为拆成一条条可自动执行的考题每次改完代码一键把所有考题重新跑一遍哪道题挂了就说明哪个功能被改坏了定位又快又准。C 里写单元测试Catch2 是当下最流行的开源框架之一GitHub 上数万 star因为它把写测试这件事变得极其轻松。二、Catch2 是什么先来个通俗类比想象你是一个老师要给 100 个学生出卷子、批卷子传统方式裸写断言Catch2 的方式自己手写考务系统测试框架Catch2 就是现成的考务系统你只管出题考砸了只报第 3 题错了自动告诉你期望 4实际得到 3在哪一行挂的想抽考一部分学生很麻烦用标签tag随便圈定只考这 5 个学生批卷结果要自己整理成报表自动输出人类可读 / XML / JUnit 多种报表Catch2 的核心口号是一个只有头文件、不需要额外配置、写起来像自然语言的测试框架。你写的测试代码长这样TEST_CASE(加法测试, [calculator]) { Calculator calc; REQUIRE(calc.add(2, 3) 5); // 像一句英语REQUIRE要求23 等于 5 }TEST_CASE声明一道考题一个测试用例REQUIRE检查一条断言失败就报错三、使用优点含对比表格3.1 与传统 assert 相比很多人初学 C 会直接用 cassert 里的 assert 写测试两者有天壤之别维度传统 assertCatch2使用目的调试期校验程序不变量这里不该发生验证功能行为这个功能该产出什么发布版行为定义 NDEBUG 后完全消失等于没测测试代码与业务代码分离发布版不含测试但测试本身永远可跑失败信息只输出文件名行号expression failed自动展开表达式左右值Expected: calc.add(2,3)5\nActual: 4组织方式零散地塞在业务代码里独立测试文件、按 TEST_CASE 分类、可打标签批量执行靠自己写 main 循环一条命令跑全部/按标签跑部分断点续查第一个 assert 失败程序就崩abortCHECK 失败不中断一条用例内能查出所有问题3.2 与其他 C 测试框架对比维度Catch2Google Test (gtest)Boost.Test上手门槛⭐ 最低宏写起来像英语文档友好中概念较多Test Suite/Fixture/Matcher高Boost 体系重宏冗长安装复杂度低v2 单头文件即用v3 一个 CMake target中需要编译 gtest 库并链接高需要引入整个 Boost测试组织TEST_CASE SECTION天然支持用例内子场景TEST_F依赖 fixture 类样板代码多BOOST_AUTO_TEST_CASE功能全但繁琐断言失败处理REQUIRE 中断 / CHECK 继续灵活切换ASSERT_* 中断 / EXPECT_* 继续BOOST_REQUIRE / BOOST_CHECK失败信息可读性表达式自动展开左右值最友好较好一般数据驱动Generators内置零额外代码需要 INSTANTIATE_TEST_SUITE_P 参数化类样板多参数化样板较多BDD 风格内置 SCENARIO/GIVEN/WHEN/THEN需要 gtest-bdd 扩展无原生支持报表输出Console / JUnit / XML / SonarQube 等XML 为主文本为主一句话总结gtest 功能全面但样板多Boost.Test 大而全但重Catch2 胜在零配置、读起来像英语、写起来最省事——特别适合中小型项目、教学和快速验证。四、使用场景算法/数据结构正确性验证排序、图算法、字符串处理把典型输入输出写成用例防回归。库与 API 的行为契约测试你发布的接口说好返回什么用测试锁死防止后续改动破坏契约。重构保护网项目要重构/升级编译器/换第三方库时先跑全量测试绿了再动手稳如老狗。教学与刷题验证学 C 时把每道题的边界条件写成 TEST_CASE比 printf 大法科学得多。CI/CD 自动化配合 CMake CTest GitHub Actions每次提交自动跑测试挂掉就拦下合并。数据驱动测试参数化同一逻辑喂多组输入边界值、随机值、边界附近值用 Generators 一行搞定。五、环境准备与安装分步骤5.1 方案 Av3 CMake FetchContent推荐现代项目标配在你的 CMakeLists.txt 中加入cmake_minimum_required(VERSION 3.14) project(Catch2Demo CXX) # 开启 C17Catch2 v3 要求 C14 以上推荐直接用 17 set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 从 GitHub 自动拉取 Catch2 源码第一次联网之后本地缓存 include(FetchContent) FetchContent_Declare( Catch2 GIT_REPOSITORY https://github.com/catchorg/Catch2.git GIT_TAG v3.5.0 # 建议固定版本避免升级带来行为变化 ) FetchContent_MakeAvailable(Catch2) # 你的测试可执行文件 add_executable(my_tests test_main.cpp) # Catch2WithMain 自带 main 函数我们不用自己写 main target_link_libraries(my_tests PRIVATE Catch2::Catch2WithMain) # 把测试注册到 CTest之后可用 ctest 一键跑 include(CTest) include(Catch) catch_discover_tests(my_tests)然后在项目根目录执行cmake -B build # 生成构建系统 cmake --build build # 编译 ctest --test-dir build # 运行所有测试CTest 会逐个调用注册的用例5.2 方案 Bv2 单头文件5 分钟极速体验Catch2 v2 只需一个头文件 catch.hpp约 1MB把它和你的测试文件放一起// test_main.cpp #define CATCH_CONFIG_MAIN // ⚠️ 关键这个宏必须放在 include 之前让 Catch2 生成 main 函数 #include catch.hpp // v2 单头文件 TEST_CASE(第一个测试, [demo]) { REQUIRE(1 1 2); }编译g -stdc17 test_main.cpp -o test_main ./test_main⚠️v2 与 v3 差异预警最易踩坑v2单头文件 catch.hpp #define CATCH_CONFIG_MAINv3拆成多个模块必须链接 Catch2::Catch2WithMain不能再写 CATCH_CONFIG_MAIN。v3 头文件路径带 catch2/ 前缀#include catch2/catch_test_macros.hpp。本文 Demo 按 v3 编写若你用的是 v2把 include 换成 catch.hpp 即可宏用法一致。六、核心概念TEST_CASE 与 SECTION6.1 TEST_CASE一道大题类比TEST_CASE 是一张试卷里的一道大题括号里写题名方括号里是标签tag——相当于这道题属于哪个知识模块。TEST_CASE(加法应该正确, [calculator][smoke]) { // 两道标签calculator 表示属于计算器模块smoke 表示冒烟测试 REQUIRE(1 1 2); }6.2 SECTION大题里的小问类比SECTION 是同一道大题下的小问。神奇之处在于每个 SECTION 都会从头开始执行整个 TEST_CASE 一次所以每个小问的环境都是独立、干净的——这是 Catch2 替代测试夹具fixture的巧妙设计。TEST_CASE(栈的 push 与 pop, [stack]) { std::vectorint v; // 每次执行小问前都会新建一个空栈干净环境 SECTION(push 之后 size 为 1) { v.push_back(42); REQUIRE(v.size() 1); // ✅ } SECTION(push 再 pop 之后为空) { v.push_back(42); v.pop_back(); REQUIRE(v.empty()); // ✅ } }执行时等价于先跑第一小问从 std::vectorint v; 开始再从头跑第二小问。所以即使第一小问里 push_back 改了 v也不会污染第二小问。⚠️SECTION 使用红线SECTION不能写在 for、while、if 等控制语句内部v2/v3 都禁止否则行为未定义编译可能报错或产生诡异结果同一层级的 SECTION 名不能重复一个 SECTION 只能嵌套在 TEST_CASE 或另一个 SECTION 内部。七、核心断言REQUIRE 与 CHECK7.1 两者区别重点类比REQUIRE 是高考一科挂科直接出局CHECK 是平时作业这题错了先记下继续做后面的题。宏失败时行为适用场景REQUIRE(expr)立即中止当前 TEST_CASE但整个程序继续跑其他用例前置条件不满足后面没法继续测CHECK(expr)记下失败继续执行当前用例后续代码想一口气收集该用例内的所有问题REQUIRE_FALSE(expr) / CHECK_FALSE(expr)断言表达式为假验证不该发生的事7.2 失败原因自动展开这是 Catch2 最惊艳的特性断言失败时自动把表达式的左右值展开你不用手动拼错误信息。TEST_CASE(失败信息示例, [demo]) { int a 3; int b 5; REQUIRE(a b); // 失败输出 // test_main.cpp:8: FAILED: // REQUIRE( a b ) // with expansion: // 3 5 ← 左右值自动展开一眼看到差异 }7.3 补充断言家族#include stdexcept #include string #include catch2/catch_test_macros.hpp TEST_CASE(异常与字符串断言, [demo]) { // 断言抛出指定类型的异常 REQUIRE_THROWS_AS(throw std::runtime_error(boom), std::runtime_error); // 断言不抛异常 REQUIRE_NOTHROW(std::string(hello)); // 用 INFO 打印上下文失败时这些日志会一起出现在报告里类比考试时在草稿纸写的过程 INFO(当前用户 id 10086); REQUIRE(true); // 字符串包含匹配Matchers类比不看整句只看有没有关键词 std::string msg hello catch2 world; REQUIRE_THAT(msg, Catch::Matchers::ContainsSubstring(catch2)); }⚠️ 浮点数不要直接 0.1 0.2 ! 0.3 是浮点精度问题。用 Catch::Approxv2或 Matchers 的 WithinRel/WithinAbsv3#include catch2/catch_approx.hpp // v3 需要显式 include double r 0.1 0.2; REQUIRE(r Catch::Approx(0.3)); // ✅ 允许微小误差八、完整可运行 Demov3下面是一个可直接运行的完整示例一个简易 BankAccount 类 完整测试文件。8.1 被测类bank_account.hpp#pragma once #include stdexcept // 简易银行账户存钱、取钱、查询余额 class BankAccount { public: explicit BankAccount(double initial) : balance_(initial) {} // 存款金额必须大于 0 void deposit(double amount) { if (amount 0) { throw std::invalid_argument(存款金额必须为正数); } balance_ amount; } // 取款金额必须大于 0且不能透支 void withdraw(double amount) { if (amount 0) { throw std::invalid_argument(取款金额必须为正数); } if (amount balance_) { throw std::runtime_error(余额不足); } balance_ - amount; } double balance() const { return balance_; } private: double balance_; // 当前余额 };8.2 测试文件test_bank.cpp// test_bank.cpp —— 编译方式见下方运行 Demo #include catch2/catch_test_macros.hpp // TEST_CASE / SECTION / REQUIRE / CHECK 都在这里 #include catch2/catch_approx.hpp // 浮点近似比较 #include bank_account.hpp // 场景一存款功能 TEST_CASE(存款功能, [bank][smoke]) { SECTION(新账户初始余额为 0) { BankAccount acc(0.0); CHECK(acc.balance() Catch::Approx(0.0)); // 用 Approx 比较浮点 } SECTION(存入 100 后余额为 100) { BankAccount acc(0.0); acc.deposit(100.0); CHECK(acc.balance() Catch::Approx(100.0)); } SECTION(存入负数抛异常) { BankAccount acc(0.0); REQUIRE_THROWS_AS(acc.deposit(-5), std::invalid_argument); } } // 场景二取款功能用 SECTION 验证用例内子场景独立 TEST_CASE(取款功能, [bank]) { BankAccount acc(100.0); // 每次小问都会从 100 元重新开始互不干扰 SECTION(取 30 后余额为 70) { acc.withdraw(30.0); REQUIRE(acc.balance() Catch::Approx(70.0)); } SECTION(取款金额超过余额抛异常) { REQUIRE_THROWS_AS(acc.withdraw(999.0), std::runtime_error); } SECTION(取款后继续取款连续操作) { acc.withdraw(40.0); acc.withdraw(10.0); CHECK(acc.balance() Catch::Approx(50.0)); } } // 场景三故意演示 CHECK 与 REQUIRE 的差异 TEST_CASE(CHECK 与 REQUIRE 行为演示, [demo]) { int a 1, b 2, c 3; CHECK(a b); // ❌ 记下失败但继续往下走 CHECK(b c); // ❌ 记下失败继续走 REQUIRE(a a); // ✅ 通过 // 若把上面任一行改成 REQUIRE 且失败这里就不会执行了 CHECK(true); }8.3 运行 Demo分步骤步骤 1建工程目录结构如下demo/ ├── CMakeLists.txt # 内容见 5.1 节把 test_main.cpp 换成 test_bank.cpp 即可 ├── bank_account.hpp └── test_bank.cpp步骤 2配置并编译cmake -B build cmake --build build步骤 3直接运行测试程序./build/test_bank.exe九、命令行参数详解测试程序的遥控器Catch2 生成的可执行文件自带一套强大的命令行参数。常用如下参数作用示例[tag] 或名字过滤只跑匹配的用例支持 * 通配./test_bank.exe [bank] 只跑银行测试-s / --success连通过的断言也打印出来./test_bank.exe -s-c section / --section只跑某个 SECTION./test_bank.exe -c 存入 100 后余额为 100-r reporter切换输出格式console / compact / junit / xml./test_bank.exe -r compact--durations yes显示每个用例耗时性能排查./test_bank.exe --durations yes-a / --abort遇到第一个失败就中止整个测试CI 快速失败./test_bank.exe -a--list-tests列出所有用例名和标签./test_bank.exe --list-tests--rng-seed n固定随机种子配合 --order rand 复现随机顺序下的失败./test_bank.exe --order rand --rng-seed 42-h查看全部帮助./test_bank.exe -h实战小技巧调试某个具体用例时用 -s -c 组合只看最小范围且把通过项也打出来快速定位问题。⚠️参数位置过滤表达式如 [bank]直接放在命令末尾作为位置参数-c 指定的 section 名必须完整精确匹配可多个 -c 叠加。十、进阶速览10.1 BDD 风格用故事写测试BDD行为驱动开发把测试写成场景-给定-当-那么更贴近业务描述。Catch2 原生支持#include catch2/catch_test_macros.hpp SCENARIO(账户取款的故事, [bank][bdd]) { GIVEN(一个余额为 100 的账户) { BankAccount acc(100.0); WHEN(我取出 30) { acc.withdraw(30.0); THEN(余额应该是 70) { REQUIRE(acc.balance() Catch::Approx(70.0)); } AND_THEN(再取 20 后余额为 50) { acc.withdraw(20.0); REQUIRE(acc.balance() Catch::Approx(50.0)); } } } }输出会以缩进的故事大纲呈现非技术同事也能看懂测试在测什么。10.2 TEMPLATE_TEST_CASE一份代码测多种类型想让同一组断言跑遍 int / double / float模板测试用例来帮忙#include catch2/catch_template_test_macros.hpp // 模板测试专用头文件 TEMPLATE_TEST_CASE(不同类型都能相加, [template], int, double, long) { TestType a 3; // TestType 依次被替换为 int、double、long TestType b 4; REQUIRE(a b TestType(7)); }它会自动生成 3 个用例int / double / long 各一个报告里用 int、double 区分。10.3 Generators数据驱动测试不用手写 for 循环用 GENERATE 批量喂数据每个数据点都是一个独立用例可单独筛选、独立报告失败#include catch2/generators/catch_generators.hpp TEST_CASE(平方函数, [gen]) { auto x GENERATE(1, 2, 3, 10); // 依次取 1,2,3,10 auto expect GENERATE(1, 4, 9, 100); // 依次取 1,4,9,100与上面同步配对 REQUIRE(x * x expect); // 生成 4 个独立用例 } TEST_CASE(范围与随机生成, [gen]) { auto n GENERATE(range(1, 5)); // 1,2,3,4 auto r GENERATE(take(10, random(0, 100))); // 取 10 个随机数可用 --rng-seed 复现 CHECK(n 0); CHECK(r 0 r 100); }⚠️配对注意多个 GENERATE 在同一作用域会按笛卡尔积/同步配对规则展开数量不一致时要小心建议保持一一对应或用 std::tie 绑定。10.4 与 CMake 集成回到 5.1catch_discover_tests(my_tests) 会把每个 TEST_CASE 单独注册成一个 CTest 用例好处ctest --test-dir build -N # 列出所有注册的测试 ctest --test-dir build -R 取款功能 # 按正则只跑某用例 ctest --test-dir build --output-on-failure # 只显示失败详情CI 最爱这样在 GitHub Actions / GitLab CI 里测试结果可以逐条展示一目了然。十一、常见问题速查表FAQ问题答案v2 和 v3 有什么区别我用哪个v2 单头文件 catch.hpp CATCH_CONFIG_MAIN上手最快v3 模块化、需链接 Catch2::Catch2WithMain、头文件带 catch2/ 前缀是当前主线。新项目推荐 v3。为什么我 #include catch.hpp 报找不到文件说明你下的是 v3头文件是 catch2/catch_test_macros.hpp或 v2 的 catch.hpp 没放到 include 路径里。编译报错 error: main must return int漏了 CATCH_CONFIG_MAINv2或没链接 Catch2::Catch2WithMainv3导致没有 main 入口。REQUIRE(a b) 失败后下面的代码还跑吗不跑。当前 TEST_CASE 立即中止但其他用例照常执行想继续跑用 CHECK。浮点比较总失败怎么办不要用 用 Catch::Approx(x)v3 记得 #include catch2/catch_approx.hpp或 Matchers 的 WithinRel。我只想跑某一个测试怎么操作./test_bank.exe 测试名 按名字过滤或 -c SECTION名 过滤小问[tag] 按标签过滤。测试用例之间会互相干扰吗不会。每个 TEST_CASE / SECTION 都是全新执行环境若需要共享资源用 static 或 fixture 要非常谨慎。为什么我的 SECTION 编译不过/行为诡异大概率把 SECTION 写进了 for/if 等控制语句里——这是禁止的必须放在顶层或另一个 SECTION 内。如何接入 CICMake 里 include(CTest) include(Catch) catch_discover_tests(...)CI 中跑 ctest --output-on-failure。生成的测试报告能进 Jenkins 吗可以用 -r junit 输出 JUnit XMLJenkins/GitLab 原生识别。测试偶发失败想复现用 --order rand --rng-seed 数字记下种子号即可复现同一随机序列。想测抛出异常怎么写REQUIRE_THROWS_AS(expr, std::runtime_error)不抛异常用 REQUIRE_NOTHROW(expr)。十二、总结Catch2 让你把写测试的成本降到最低三个宏起步TEST_CASE REQUIRE CHECK 就能建立安全网SECTION 自动隔离天然解决每个用例独立环境的难题失败信息自动展开省去手拼日志命令行参数 CMake/CTest 集成从本地调试到 CI 一键打通BDD / 模板测试 / Generators满足进阶数据驱动需求。把测试当作代码的安全网而不是额外负担——从这个 Demo 开始给下一个类补上第一张安全网吧。