核心目标:掌握 Catch2 单元测试框架的核心概念与使用方法,学会编写高质量的 C++ 单元测试,建立可靠的测试驱动开发(TDD)工作流。
前置知识:熟悉 C++11/14/17 基础语法;了解基本的构建工具(CMake)。
适用版本:Catch2 v3.x(本指南以 v3.7.0 为基准,部分内容兼容 v2.x)
1. 单元测试与 Catch2 概述
1.1 什么是单元测试
单元测试(Unit Testing)是对软件中的最小可测试单元(通常是函数或类方法)进行正确性验证的实践。其核心理念是将被测单元从外部依赖中隔离出来,通过输入 → 断言输出的方式验证行为。
通过
失败
被测试单元
(函数/方法)
测试框架
(Catch2)
断言结果
✅ 测试成功
❌ 测试失败
输出预期 vs 实际
单元测试的三条黄金法则:
| 法则 | 说明 |
|---|---|
| 可重复 | 任意顺序、任意次数运行,结果一致 |
| 独立 | 测试之间互不依赖,不共享可变状态 |
| 快速 | 毫秒级执行,让开发者愿意频繁运行 |
1.2 为什么选择 Catch2
Catch2 是 C++ 社区最受欢迎的现代单元测试框架之一,与其他主流框架的对比:
| 对比维度 | Catch2 | Google Test | Boost.Test | doctest |
|---|---|---|---|---|
| C++ 标准 | C++14+ | C++14+ | C++11+ | C++11+ |
| 头文件方式 | ✅ 支持 | ❌ 需编译 | ❌ 需编译 | ✅ 支持 |
| BDD 风格 | ✅ 原生 | ✅ 需扩展 | ❌ | ✅ |
| 数据生成器 | ✅ 丰富 | ⚠️ 有限 | ❌ | ⚠️ 基础 |
| 注册方式 | 自动注册 | 自动注册 | 手动注册 | 自动注册 |
| 编译速度 | ⭐⭐⭐ | ⭐⭐ | ⭐ | ⭐⭐⭐⭐ |
Catch2 核心优势:
- 零配置:引入单个头文件即可开始测试
- 现代 C++ 风格:充分利用 lambda、模板、表达式模板等语言特性
- 自描述断言 :
REQUIRE(a == b)失败时自动输出 a 和 b 的值- 灵活的标签系统:支持按任意维度组织、筛选测试
- 强大的数据生成器:无需额外框架即可实现参数化测试
1.3 Catch2 版本演进
| 版本 | 发布时间 | 关键变化 |
|---|---|---|
| v1.x | 2012--2019 | 单头文件 catch.hpp,C++11 兼容 |
| v2.x | 2019--2023 | 可选的预编译模式,重构断言引擎 |
| v3.x | 2022--至今 | 架构重写 ,改为多文件库,支持 CMake 安装;Catch2::Catch2 target;移除了旧版宏 |
注意 :v3.x 不再提供单头文件
catch.hpp下载,而是提供 CMake 安装包和catch_amalgamated.hpp/catch_amalgamated.cpp合并文件。本指南以 v3.x 为准。
2. 安装与工程配置
2.1 头文件方式(Header-Only)
Catch2 v2.x 可直接使用单头文件方式:
bash
# 下载单头文件
wget https://raw.githubusercontent.com/catchorg/Catch2/devel/extras/catch_amalgamated.hpp
wget https://raw.githubusercontent.com/catchorg/Catch2/devel/extras/catch_amalgamated.cpp
将 catch_amalgamated.hpp 放入项目 include/ 目录,catch_amalgamated.cpp 放入 src/ 目录参与编译即可。
2.2 CMake FetchContent 集成
推荐使用 CMake FetchContent 自动管理 Catch2 依赖:
cmake
cmake_minimum_required(VERSION 3.20)
project(MyProject LANGUAGES CXX)
# ---------- Catch2 ----------
include(FetchContent)
FetchContent_Declare(
Catch2
GIT_REPOSITORY https://github.com/catchorg/Catch2.git
GIT_TAG v3.7.0
)
# 重要:测试构建仅在开启 BUILD_TESTING 时才拉取 Catch2
if(BUILD_TESTING)
FetchContent_MakeAvailable(Catch2)
list(APPEND CMAKE_MODULE_PATH ${catch2_SOURCE_DIR}/extras)
include(CTest)
enable_testing()
endif()
# ---------- 主库 ----------
add_library(my_lib src/my_lib.cpp include/my_lib.hpp)
target_include_directories(my_lib PUBLIC include)
# ---------- 测试 ----------
if(BUILD_TESTING)
add_executable(my_tests
tests/test_main.cpp
tests/test_math.cpp
tests/test_string.cpp
)
target_link_libraries(my_tests PRIVATE my_lib Catch2::Catch2WithMain)
add_test(NAME my_tests COMMAND my_tests)
endif()
提示 :链接
Catch2::Catch2WithMain会自动包含默认的main()实现,无需自己编写。
2.3 vcpkg 包管理集成
如已安装 vcpkg,可直接安装 Catch2:
bash
# 安装 Catch2 v3.x
vcpkg install catch2
# 安装 Catch2 v2.x(旧版)
vcpkg install catch2[x64-windows]
CMake 中通过 toolchain 文件使用:
cmake
cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=[vcpkg-root]/scripts/buildsystems/vcpkg.cmake
2.4 项目目录结构建议
推荐以下目录组织方式:
my_project/
├── include/ # 公共头文件
│ └── my_lib/
│ ├── math.h
│ └── string_util.h
├── src/ # 源代码
│ ├── math.cpp
│ └── string_util.cpp
├── tests/ # 测试代码
│ ├── CMakeLists.txt
│ ├── test_main.cpp # 可选:自定义 main
│ ├── test_math.cpp
│ ├── test_string_util.cpp
│ ├── fixtures/
│ │ └── test_database.cpp
│ └── generators/
│ └── test_data_driven.cpp
├── benchmarks/ # 基准测试
├── CMakeLists.txt
└── README.md
说明 :建议将
tests/作为独立子目录,每个被测模块对应一个测试文件,便于维护和并行编译。
3. 第一个测试用例
3.1 最小测试程序
下面是一个完整的 Catch2 测试程序,测试一个简单的数学函数:
cpp
// tests/test_math.cpp
#include <catch2/catch_test_macros.hpp>
// 被测函数
int add(int a, int b) {
return a + b;
}
TEST_CASE("加法运算", "[math][core]") {
REQUIRE(add(1, 2) == 3);
REQUIRE(add(-1, 1) == 0);
REQUIRE(add(0, 0) == 0);
REQUIRE(add(100, 200) == 300);
}
编译运行:
bash
# 假设已按 2.2 节配置好 CMake
cmake --build build
cd build && ctest --output-on-failure
# 或直接运行测试程序
./tests/my_tests
输出示例:
All tests passed (4 assertions in 1 test case)
若断言失败:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
my_tests is a Catch2 v3.7.0 host application.
Run with -? for help
-------------------------------------------------------------------------------
加法运算
-------------------------------------------------------------------------------
my_project/tests/test_math.cpp:10
...............................................................................
my_project/tests/test_math.cpp:10: FAILED:
REQUIRE( add(-1, 1) == 0 )
with expansion:
add(-1, 1) == 0
with expansion:
0 == 0
注意 :Catch2 会自动展开表达式,显示
REQUIRE(add(-1, 1) == 0)的展开值为0 == 0。如果实际值出错,它会显示成2 == 0这样的直观信息。这就是 Catch2 的------"自描述断言"特性。
3.2 断言宏体系
Catch2 提供两套断言宏,满足不同场景:
| 宏 | 行为 | 使用场景 |
|---|---|---|
REQUIRE(expr) |
断言失败则停止当前测试用例 | 前置条件检查,后续依赖该条件 |
CHECK(expr) |
断言失败后继续执行后续断言 | 收集多个失败信息 |
REQUIRE_FALSE(expr) |
断言表达式为假 | REQUIRE(!expr) 的语法糖 |
CHECK_FALSE(expr) |
检查表达式为假,失败后继续 | 同上 |
对比示例:
cpp
TEST_CASE("REQUIRE vs CHECK 对比", "[assert]") {
int a = 1, b = 2, c = 3;
CHECK(a + b == 3); // ✅ 通过
CHECK(a + c == 5); // ❌ 失败,但继续执行
CHECK(b + c == 5); // ✅ 通过(会被执行到)
int d = 4;
REQUIRE(d == 4); // ✅ 通过
REQUIRE(d > 10); // ❌ 失败,测试用例立即停止
REQUIRE(d == 4); // 这行不会被执行到
}
值比较宏(v3.x 推荐使用表达式模板而非传统比较宏,但仍支持):
REQUIRE_THAT(value, matcher) // 使用 Matcher 进行断言(详见第 6 章)
3.3 SECTION ------ 测试用例内的场景划分
SECTION 是 Catch2 区别于其他框架的核心特性------同一 TEST_CASE 内部的多个 SECTION 会在各自独立的上下文中执行,且共享前置代码:
cpp
TEST_CASE("std::vector 操作", "[container]") {
std::vector<int> v;
SECTION("空 vector 的大小为零") {
REQUIRE(v.size() == 0);
REQUIRE(v.empty());
}
SECTION("push_back 后 size 增加") {
v.push_back(42);
REQUIRE(v.size() == 1);
REQUIRE(v[0] == 42);
}
SECTION("重复 push_back 会覆盖?不会") {
v.push_back(1);
v.push_back(2);
REQUIRE(v.size() == 2);
REQUIRE(v[0] == 1);
REQUIRE(v[1] == 2);
}
}
SECTION 的执行模型(非常重要):
SECTION 1
SECTION 2
SECTION 3
是,重新进入
否
TEST_CASE 入口
执行前置代码
(std::vector<int> v;)
选择 SECTION
SECTION 【空 vector】
SECTION 【push_back】
SECTION 【重复 push_back】
结束
还有未执行的
SECTION 吗?
TEST_CASE 结束
每个 SECTION 都是独立运行的,每次运行前都会重新执行 SECTION 之前的代码。这意味着:
SECTION之间不会互相干扰- 前置代码在每个
SECTION之前都重新执行 SECTION支持嵌套,形成"场景树"
SECTION 嵌套示例:
cpp
TEST_CASE("嵌套 SECTION", "[section]") {
std::string s = "hello";
SECTION("检查长度") {
REQUIRE(s.length() == 5);
SECTION("检查每个字符") {
REQUIRE(s[0] == 'h');
REQUIRE(s[1] == 'e');
}
}
SECTION("修改字符串") {
s += " world";
SECTION("修改后长度") {
REQUIRE(s.length() == 11);
}
}
}
嵌套 SECTION 的执行路径数为:所有叶子节点的数量 。上面的例子有 3 条路径:长度 → 字符、修改 → 修改后长度。
4. 测试组织与分层
4.1 TEST_CASE 命名规范
TEST_CASE 的名称可以包含空格,建议遵循:[被测模块][测试场景][期望行为]
TEST_CASE("StringUtil: trim removes leading/trailing spaces", "[string]")
TEST_CASE("Math: sqrt returns correct value for perfect squares", "[math]")
TEST_CASE("UserService: login fails with wrong password", "[user][auth]")
| 测试级别 | 命名风格 | 示例 |
|---|---|---|
| 单元级 | [模块]: [行为描述] |
Math: add works for negative numbers |
| 集成级 | [模块A] + [模块B]: [交互描述] |
Database + Cache: read returns cached value |
| 回归级 | Issue #[编号]: [问题描述] |
Issue #42: overflow on large input |
4.2 测试套件:TAG 系统
Catch2 使用标签(Tag)而非传统测试套件来组织测试。标签写在 TEST_CASE 第二个参数的方括号中:
cpp
TEST_CASE("基本数学运算", "[math][core]") { }
TEST_CASE("高级数学运算", "[math][advanced]") { }
TEST_CASE("字符串处理", "[string][core]") { }
标签系统的优势:
| 操作 | 命令行 |
|---|---|
仅运行 [math] 标签 |
./my_tests [math] |
运行 [math] 且 [core] |
./my_tests [math][core] |
运行 [math] 或 [string] |
./my_tests "[math],[string]" |
排除 [slow] 标签 |
./my_tests ~[slow] |
| 复杂表达式 | `./my_tests "([math] |
4.3 标签表达式与选择运行
标签表达式支持布尔运算,用于 CI 中的分阶段测试:
bash
# 运行所有非慢速测试(日常开发快速验证)
./my_tests ~[slow]
# 运行所有 [math] 标签且排除 [advanced]
./my_tests "[math] && ~[advanced]"
# 运行 [integration] 或 [e2e] 标签的测试
./my_tests "[integration],[e2e]"
# 仅运行名称为关键字的测试
./my_tests "StringUtil"
自动化标签分层建议:
[unit] 基础单元测试,执行 < 10ms
[integration] 集成测试,涉及真实 I/O 或外部服务
[slow] 耗时 > 100ms 的测试,CI 中可选
[ci] 每次 CI 必须通过的测试
[regression] 回归测试,修复过 Bug 的用例
cpp
// 开发提测阶段 ------ 快速验证
./my_tests "[unit] && ~[slow]"
// CI 完整验证
./my_tests "[ci] || [unit]"
// 夜间全部运行
./my_tests
5. BDD 风格测试
5.1 GIVEN / WHEN / THEN 语法
Catch2 原生支持 BDD(Behavior-Driven Development,行为驱动开发)风格的测试,将测试组织为 Given-When-Then 三部曲:
cpp
#include <catch2/catch_test_macros.hpp>
#include <catch2/catch_message.hpp>
// 被测类
class BankAccount {
double balance_;
public:
explicit BankAccount(double initial) : balance_(initial) {}
void deposit(double amount) { balance_ += amount; }
bool withdraw(double amount) {
if (amount > balance_) return false;
balance_ -= amount;
return true;
}
double balance() const { return balance_; }
};
SCENARIO("银行账户提款", "[bank][bdd]") {
GIVEN("一个有 100 元的账户") {
BankAccount account(100.0);
WHEN("提取 50 元") {
bool result = account.withdraw(50.0);
THEN("提款成功,余额为 50 元") {
REQUIRE(result == true);
REQUIRE(account.balance() == 50.0);
}
}
WHEN("提取 200 元") {
bool result = account.withdraw(200.0);
THEN("提款失败,余额仍为 100 元") {
REQUIRE(result == false);
REQUIRE(account.balance() == 100.0);
}
}
}
}
SCENARIO 是 TEST_CASE 的别名,GIVEN/WHEN/THEN/AND_WHEN/AND_THEN 都是 SECTION 的别名------它们在本质上与 SECTION 完全相同,只是语义更清晰。
5.2 与其他测试框架的风格对比
| 阶段 | Catch2 | Google Test (gtest) | SpecFlow (.NET) |
|---|---|---|---|
| Given 前置条件 | GIVEN("条件") {...} |
// 在 TEST_F 中手动设置 |
[Given(@"条件")] |
| When 操作 | WHEN("操作") {...} |
// 调用被测方法 |
[When(@"操作")] |
| Then 验证 | THEN("结果") {...} |
EXPECT_EQ(...) |
[Then(@"结果")] |
提示:BDD 风格的代码在生成测试报告时更容易被非技术人员理解,特别适合作为"可执行的规格说明"。
6. Matchers(匹配器)
6.1 内置 Matchers 一览
Matchers 提供比运算符更精确、更具表达力的断言方式:
| 类别 | Matcher | 说明 |
|---|---|---|
| 比较 | IsEqual(x) |
等价于 == x |
IsNotEqual(x) |
不等价 | |
| 浮点数 | WithinAbs(margin) |
绝对误差范围 |
WithinRel(percentage) |
相对误差百分比 | |
WithinULP(ulps) |
ULP(最小精度单位)误差 | |
| 字符串 | ContainsSubstring(str) |
包含子串 |
StartsWith(str) |
以...开头 | |
EndsWith(str) |
以...结尾 | |
Matches(pattern) |
正则匹配 | |
| 范围/集合 | IsEmpty() |
容器为空 |
SizeIs(n) |
容器大小为 n | |
SizeIs(Lower, Upper) |
容器大小在 [Lower, Upper] 范围 | |
VectorContains(elem) |
vector 包含某元素 | |
UnorderedRangeEquals(...) |
无序比较集合相等 | |
| 谓词 | Predicate<T>(callable, desc) |
自定义谓词 |
| 异常 | Throws<T>(...matcher...) |
异常断言(v3.x 新增) |
6.2 字符串 Matchers
cpp
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
using namespace Catch::Matchers;
TEST_CASE("字符串匹配器", "[matcher][string]") {
std::string text = "Hello, Catch2 World!";
CHECK_THAT(text, ContainsSubstring("Catch2"));
CHECK_THAT(text, StartsWith("Hello"));
CHECK_THAT(text, EndsWith("World!"));
CHECK_THAT(text, !StartsWith("World"));
// 正则匹配
CHECK_THAT(text, Matches("Hello, \\w+ \\w+!"));
// 组合匹配
CHECK_THAT(text, ContainsSubstring("Catch2") && ContainsSubstring("Hello"));
}
6.3 浮点数比较
浮点数无法直接用 == 比较,Catch2 提供了三种方式:
cpp
#include <catch2/catch_approx.hpp>
TEST_CASE("浮点数比较", "[float][matcher]") {
double a = 0.1 + 0.2; // 实际为 0.30000000000000004
double b = 0.3;
// 方式一:Approx(v2.x 兼容)
REQUIRE(a == Approx(b).margin(1e-10));
// 方式二:WithinAbs 绝对误差(v3.x 推荐)
REQUIRE_THAT(a, WithinAbs(b, 1e-10));
// 方式三:WithinRel 相对误差
REQUIRE_THAT(a, WithinRel(b, 1e-10));
// 方式四:WithinULP ULP 精度
REQUIRE_THAT(a, WithinULP(b, 2));
}
| 方法 | 适用场景 | 优势 |
|---|---|---|
WithinAbs(a, margin) |
值在 0 附近的比较 | 固定误差容限 |
WithinRel(pct) |
值较大时 | 误差与值的大小成比例 |
WithinULP(ulps) |
严格数学计算 | ULP 是浮点数理论最小精度单位 |
6.4 自定义 Matcher
当内置 Matcher 不满足需求时,可以继承 Catch::Matchers::MatcherBase<T> 自定义:
cpp
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers.hpp>
// 自定义 Matcher:验证 vector 是否有序
class IsSortedMatcher : public Catch::Matchers::MatcherBase<std::vector<int>> {
public:
IsSortedMatcher() = default;
bool match(const std::vector<int>& v) const override {
for (size_t i = 1; i < v.size(); ++i) {
if (v[i - 1] > v[i]) return false;
}
return true;
}
std::string describe() const override {
return "is sorted in ascending order";
}
};
// 工厂函数(必须提供)
IsSortedMatcher IsSorted() {
return IsSortedMatcher();
}
TEST_CASE("自定义 Matcher 示例", "[matcher][custom]") {
std::vector<int> data = {1, 2, 3, 5, 8};
CHECK_THAT(data, IsSorted());
std::vector<int> bad = {3, 1, 2};
// 失败时将输出:bad is sorted in ascending order
CHECK_THAT(bad, IsSorted());
}
失败输出示例:
...............................................................................
custom_matcher.cpp:17: FAILED:
CHECK_THAT( bad, IsSorted() )
with expansion:
{ 3, 1, 2 } is sorted in ascending order
7. 测试夹具(Fixtures)
7.1 struct/class 作为夹具
当多个测试用例需要共享相同的设置(SetUp)和清理(TearDown)逻辑时,可以使用类夹具:
cpp
#include <catch2/catch_test_macros.hpp>
class DatabaseFixture {
protected:
DatabaseConnection conn_;
DatabaseFixture() : conn_("localhost:3306", "test_user", "test_pass") {
// 类似于 SetUp:每个 TEST_CASE 方法前执行
conn_.connect();
conn_.execute("CREATE TABLE IF NOT EXISTS test_data (id INT, val TEXT)");
}
~DatabaseFixture() {
// 类似于 TearDown:每个 TEST_CASE 方法后执行
conn_.execute("DROP TABLE IF EXISTS test_data");
conn_.disconnect();
}
void insert(int id, const std::string& val) {
conn_.execute("INSERT INTO test_data VALUES (" + std::to_string(id) + ", '" + val + "')");
}
};
// 使用方法:TEST_CASE_METHOD(夹具类, "名称", "[tag]")
TEST_CASE_METHOD(DatabaseFixture, "插入单条记录", "[db][integration]") {
insert(1, "hello");
auto result = conn_.query("SELECT * FROM test_data WHERE id = 1");
REQUIRE(result.size() == 1);
REQUIRE(result[0]["val"] == "hello");
}
TEST_CASE_METHOD(DatabaseFixture, "插入重复 ID 会覆盖", "[db][integration]") {
insert(1, "first");
insert(1, "second");
auto result = conn_.query("SELECT * FROM test_data WHERE id = 1");
REQUIRE(result.size() == 1);
REQUIRE(result[0]["val"] == "second");
}
说明 :构造函数/析构函数分别扮演
SetUp/TearDown的角色。Catch2 不提供类似SetUp()/TearDown()的虚函数机制,而是直接依赖 C++ 的 RAII 语义。
7.2 TEMPLATE_TEST_CASE ------ 模板测试
当同一个测试逻辑需要在多个类型上运行时,使用模板测试:
cpp
#include <catch2/catch_test_macros.hpp>
// 声明模板测试用例,注册多个类型
TEMPLATE_TEST_CASE("所有整数类型的加法同态", "[template][math]", int, unsigned, long, long long) {
TestType a = 10;
TestType b = 20;
TestType c = a + b;
// (a + b) + c == a + (b + c)
REQUIRE((a + b) + c == a + (b + c));
// (a + b) * c == a*c + b*c(分配律)
REQUIRE((a + b) * c == a * c + b * c);
}
更多模板宏:
| 宏 | 用途 |
|---|---|
TEMPLATE_TEST_CASE(sig, tags, type1, type2, ...) |
类型列表测试 |
TEMPLATE_PRODUCT_TEST_CASE(sig, tags, (T1,T2), (U1,U2)) |
类型笛卡尔积 |
TEMPLATE_LIST_TEST_CASE(sig, tags, type_list) |
使用 std::tuple 类型列表 |
7.3 GENERATOR ------ 数据驱动测试
GENERATOR 是 Catch2 最强大的特性之一,它允许以声明式的方式生成海量测试数据,而无需手动编写循环:
cpp
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <catch2/generators/catch_generators_range.hpp>
TEST_CASE("平方根计算(数据驱动)", "[gen][math]") {
// 基本生成器
auto input = GENERATE(0, 1, 4, 9, 16, 25, 100);
INFO("测试输入: " << input);
double result = std::sqrt(static_cast<double>(input));
REQUIRE(result * result == Approx(input).margin(1e-10));
}
高阶生成器用法:
cpp
TEST_CASE("生成器组合", "[gen][advanced]") {
// 从容器生成
auto data = GENERATE_COPY(
from_range(std::vector<int>{1, 2, 3}),
values({10, 20, 30}),
random(-100, 100)
);
// 生成一对输入
auto [input, expected] = GENERATE(
table<int, int>({
{1, 1},
{2, 4},
{3, 9},
{4, 16},
{10, 100}
})
);
REQUIRE(input * input == expected);
}
| 生成器 | 说明 |
|---|---|
GENERATE(a, b, c, ...) |
显式枚举值 |
from_range(begin, end) |
从迭代器范围生成 |
from_range(container) |
从容器生成 |
values({a, b, c}) |
初始化列表 |
random(lower, upper) |
随机整数 |
table<A, B>({...}) |
生成成对参数 |
filter(pred, generator) |
过滤符合条件的值 |
take(n, generator) |
取前 n 个值 |
repeat(n, generator) |
重复 n 次 |
8. 命令行选项与运行控制
8.1 常用命令行选项
./my_tests -? # 显示帮助
./my_tests -l # 列出所有测试用例及其标签
./my_tests --list-tests # 列出所有测试用例(同 -l)
./my_tests --list-tags # 列出所有标签
./my_tests --verbosity high # 详细输出级别
# 选择测试
./my_tests "[math]" # 运行带 [math] 标签的测试
./my_tests "test case name" # 运行名称中包含此字符串的测试
./my_tests "test*" # 支持通配符 *
# 控制测试执行
./my_tests --order decl # 按声明顺序(默认)
./my_tests --order lex # 按字典序
./my_tests --order rand # 随机顺序
./my_tests --rng-seed 42 # 设置随机种子
./my_tests --shard-count 4 --shard-index 0 # 分片运行(用于 CI 并行)
# 失败处理
./my_tests --abort # 首个失败即退出
./my_tests -x # 同上(缩写)
./my_tests --max-failures 5 # 累计 5 个失败后退出
# 输出
./my_tests --out test_results.txt # 输出到文件
./my_tests -o test_results.xml # 输出到文件(自动识别扩展名)
./my_tests --durations yes # 显示每个测试耗时
./my_tests --durations no # 不显示耗时
8.2 随机数与种子
随机测试可以帮助暴露依赖执行顺序的隐性 Bug:
bash
# 首次运行:自动生成随机种子
./my_tests --order rand --rng-seed time
# 每次运行结果都不一样
# 某次失败后,运行日志会显示种子值
# e.g. test case '...' ordered by 'random' with seed '12345'
# 用相同种子复现失败
./my_tests --order rand --rng-seed 12345
最佳实践 :所有 CI 构建都应使用
--order rand运行测试,确保测试之间真正的独立性。
8.3 输出格式与报告
| 格式 | 命令行 | 用途 |
|---|---|---|
| 控制台 | 默认 | 日常开发 |
| JUnit XML | ./my_tests -r junit -o results.xml |
CI 集成(Jenkins、GitLab CI) |
| JSON | ./my_tests -r json -o results.json |
自定义分析工具 |
| 紧凑 | ./my_tests -r compact |
快速概览 |
JUnit XML 输出可用于主流的 CI/CD 平台解析:
xml
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite name="my_tests" errors="0" failures="1" tests="42" hostname="localhost" time="0.512" timestamp="2026-05-18T07:56:00Z">
<testcase name="加法运算" classname="[math][core]" time="0.001">
<failure message="REQUIRE(add(-1, 1) == 0) FAILED" type="REQUIRE">
add(-1, 1) == 0
with expansion:
2 == 0
</failure>
</testcase>
</testsuite>
</testsuites>
9. CMake 集成最佳实践
9.1 CTest 集成
通过 add_test 注册到 CTest 后,可以使用 ctest 命令统一管理所有测试:
cmake
# tests/CMakeLists.txt
# 将所有 .cpp 编译为一个测试二进制,或按需拆分为多个
# 方式一:单二进制
add_executable(all_tests
test_main.cpp
test_math.cpp
test_string.cpp
test_database.cpp
)
target_link_libraries(all_tests PRIVATE my_lib Catch2::Catch2WithMain)
add_test(NAME all_tests COMMAND all_tests --order rand --verbosity high)
# 方式二:按模块拆分(推荐,可并行编译)
add_executable(math_tests test_math.cpp)
target_link_libraries(math_tests PRIVATE my_lib Catch2::Catch2WithMain)
add_test(NAME math_tests COMMAND math_tests [math])
add_executable(string_tests test_string.cpp)
target_link_libraries(string_tests PRIVATE my_lib Catch2::Catch2WithMain)
add_test(NAME string_tests COMMAND string_tests [string])
# ===== CTest 标签映射 =====
set_tests_properties(math_tests PROPERTIES LABELS "unit;math")
set_tests_properties(string_tests PROPERTIES LABELS "unit;string")
CTest 常用命令:
bash
ctest # 运行所有注册的测试
ctest -N # 列出所有测试(不运行)
ctest -R "math" # 运行名称包含 math 的测试
ctest -L "unit" # 运行标签为 unit 的测试
ctest -E "slow" # 排除名称包含 slow 的测试
ctest --output-on-failure # 失败时输出详细信息
ctest -j 4 # 4 个任务并行运行
ctest --repeat-until-fail 5 # 重复运行直到失败(找 flaky test)
9.2 覆盖率收集(gcov/lcov)
CMake 中启用覆盖率的典型配置:
cmake
# CMakeLists.txt
if(ENABLE_COVERAGE)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} --coverage -fprofile-arcs -ftest-coverage")
target_link_options(my_lib PRIVATE --coverage)
endif()
bash
# 生成覆盖率报告
cmake -B build -S . -DENABLE_COVERAGE=ON
cmake --build build
cd build && ctest --output-on-failure
# 使用 gcovr 生成 HTML 报告
gcovr -r .. --html --html-details -o coverage_report.html
# 或使用 lcov
lcov --capture --directory . --output-file coverage.info
genhtml coverage.info --output-directory coverage_html
9.3 CI/CD 流水线集成
GitHub Actions 示例:
yaml
# .github/workflows/test.yml
name: Unit Tests
on: [push, pull_request]
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
build_type: [Debug, Release]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
repository: catchorg/Catch2
path: catch2
- name: Configure
run: |
cmake -B build \
-DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \
-DBUILD_TESTING=ON
- name: Build
run: cmake --build build --parallel
- name: Test
run: ctest --test-dir build --output-on-failure -j $(nproc)
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-${{ matrix.os }}-${{ matrix.build_type }}
path: build/**/*.xml
10. 高级特性
10.1 自定义 Main 函数
当需要自定义测试入口(如初始化日志系统、解析额外参数、注册自定义报告器)时:
cpp
// tests/custom_main.cpp
#define CATCH_CONFIG_RUNNER // 告诉 Catch2 不生成 main
#include <catch2/catch_all.hpp>
int main(int argc, char* argv[]) {
// 1. 自定义初始化
Logger::init("test.log");
INFO("测试开始于 " << Logger::timestamp());
// 2. 启动 Catch2 会话
Catch::Session session;
// 3. 注册自定义命令行选项
int custom_option = 0;
auto cli = session.cli()
| Catch::Clara::Opt(custom_option, "value")
["--my-option"]
("自定义选项描述");
session.cli(cli);
// 4. 解析命令行
int returnCode = session.applyCommandLine(argc, argv);
if (returnCode != 0) return returnCode;
// 5. 使用自定义选项
if (custom_option > 0) {
INFO("自定义选项值: " << custom_option);
}
// 6. 运行测试
return session.run();
}
然后编译时不 链接 Catch2::Catch2WithMain,改为链接 Catch2::Catch2:
cmake
add_executable(my_tests
custom_main.cpp
test_math.cpp
test_string.cpp
)
target_link_libraries(my_tests PRIVATE my_lib Catch2::Catch2)
10.2 自定义 Reporter
Reporters 控制测试结果的输出格式。Catch2 提供 ConsoleReporter、JunitReporter、CompactReporter、JsonReporter。你也可以自定义:
cpp
#include <catch2/reporters/catch_reporter_streaming_base.hpp>
#include <catch2/reporters/catch_reporter_registrars.hpp>
class SimpleProgressReporter : public Catch::StreamingReporterBase {
public:
using StreamingReporterBase::StreamingReporterBase;
// 测试用例开始
void testRunStarting(Catch::TestRunInfo const&) override {
stream() << "=== 测试运行开始 ===\n";
}
// 测试用例通过
void testCasePartialEnded(Catch::TestCaseStats const& stats, uint64_t) override {
if (stats.totals.assertions.allOk()) {
stream() << ".";
} else {
stream() << "F";
}
}
// 测试用例失败
void testRunEnded(Catch::TestRunStats const& stats) override {
stream() << "\n=== 共" << stats.totals.assertions.passed
<< "通过," << stats.totals.assertions.failed << "失败 ===\n";
}
static std::string getDescription() {
return "简洁进度条报告器";
}
};
// 注册自定义 Reporter
CATCH_REGISTER_REPORTER("simple", SimpleProgressReporter)
使用:
bash
./my_tests -r simple
# 输出:=== 测试运行开始 ===
# ..........F..........
# === 共 22 通过,1 失败 ===
10.3 Approx ------ 近似比较
Approx 是 Catch2 v2.x/v3.x 都支持的浮点数比较工具:
cpp
#include <catch2/catch_approx.hpp>
TEST_CASE("Approx 使用详解", "[float][approx]") {
double result = 1.0 / 3.0 * 3.0;
// 基本用法
REQUIRE(result == Approx(1.0));
// 自定义精度
REQUIRE(result == Approx(1.0).margin(1e-12)); // 绝对误差
REQUIRE(result == Approx(1.0).epsilon(0.001)); // 相对误差
// 链式调用
REQUIRE(123456789.0 == Approx(123456780.0).epsilon(0.0001));
}
| 方法 | 含义 | 默认值 |
|---|---|---|
.epsilon(rel) |
相对误差容限 | std::numeric_limits<float>::epsilon() * 100 |
.margin(abs) |
绝对误差容限(值在 0 附近时使用) | 0.0 |
.scale(s) |
缩放因子 | 0.0(自动计算) |
建议 :对于接近 0 的值使用
.margin(),对于较大的值使用.epsilon()。
10.4 异常测试
Catch2 提供简洁的异常断言语法:
cpp
#include <stdexcept>
void mightThrow(int x) {
if (x < 0) throw std::invalid_argument("x 不能为负数");
if (x == 0) throw std::runtime_error("x 不能为零");
}
TEST_CASE("异常测试", "[exception]") {
// 断言抛出指定异常类型
REQUIRE_THROWS_AS(mightThrow(-1), std::invalid_argument);
// 断言抛出任意异常
REQUIRE_THROWS(mightThrow(-1));
// 断言不抛出异常
REQUIRE_NOTHROW(mightThrow(42));
// 断言异常消息匹配模式(v3.x)
REQUIRE_THROWS_WITH(mightThrow(0), ContainsSubstring("不能为零"));
// 获取异常对象进行进一步断言
REQUIRE_THROWS_AS(mightThrow(-1), std::invalid_argument);
}
注意 :
REQUIRE_NOTHROW是非常有用的安全断言------当你重构代码后,可以用它确保某个路径不会意外抛出异常。
11. 最佳实践与常见模式
11.1 测试代码组织结构
tests/
├── unit/ # 纯单元测试:无外部依赖,全部 mock
│ ├── test_math.cpp
│ ├── test_string.cpp
│ └── test_config.cpp
├── integration/ # 集成测试:依赖真实 DB、网络
│ ├── test_database.cpp
│ └── test_network.cpp
├── regression/ # 回归测试:Bug 修复专用
│ └── issue_042.cpp
├── fixtures/ # 共享夹具
│ ├── temp_dir_fixture.h
│ └── db_fixture.h
├── mocks/ # Mock 定义
│ └── mock_http_client.h
├── helpers/ # 测试工具函数
│ └── test_utils.h
├── CMakeLists.txt # 测试项目构建文件
└── test_main.cpp # 自定义 Main(可选)
11.2 测试命名与粒度
好的测试命名(描述行为,而非实现):
cpp
// ✅ 好的命名
TEST_CASE("StringUtil: trim removes leading whitespace", "[string]")
TEST_CASE("StringUtil: trim removes trailing whitespace", "[string]")
TEST_CASE("StringUtil: trim preserves internal spaces", "[string]")
TEST_CASE("StringUtil: trim handles empty string", "[string][edge]")
// ❌ 差的命名
TEST_CASE("测试 trim 函数", "[string]") // 太模糊
TEST_CASE("trim 功能", "[string]") // 未说明验证点
TEST_CASE("bug fix", "[string]") // 无从知晓 Bug 内容
测试粒度原则:
- 一个
TEST_CASE测试一个逻辑概念(加法、清零、边界条件) - 一个
TEST_CASE内的每个SECTION测试一种场景 - 测试代码行数不应超过被测代码的 3-5 行时,应合并或用 GENERATOR
11.3 测试替身(Mock/Stub/Fake)策略
Catch2 不内建 Mock 框架,推荐以下组合方式:
| 场景 | 推荐方案 |
|---|---|
| 简单的虚拟实现 | 手写 Stub 类 |
| 纯虚接口 Mock | FakeIt(轻量级,与 Catch2 配合极佳) |
| C++20 概念约束 Mock | trompeloeil |
| 通用 Mock | Google Mock(搭配 Catch2 需要额外配置) |
FakeIt 与 Catch2 配合示例:
cpp
#include <catch2/catch_test_macros.hpp>
#include <fakeit.hpp>
struct HttpInterface {
virtual int get(const std::string& url) = 0;
virtual ~HttpInterface() = default;
};
TEST_CASE("使用 FakeIt Mock 接口", "[mock]") {
fakeit::Mock<HttpInterface> mock;
// 设置期望
fakeit::When(Method(mock, get).Using("http://api.example.com"))
.Return(200);
// 注入并使用
HttpInterface& http = mock.get();
int code = http.get("http://api.example.com");
REQUIRE(code == 200);
// 验证调用次数
fakeit::Verify(Method(mock, get).Using("http://api.example.com"))
.Exactly(1);
}
11.4 应避免的陷阱
| 陷阱 | 说明 | 正确做法 |
|---|---|---|
| 依赖测试顺序 | 测试 B 假设测试 A 已先执行 | 每个测试独立创建所需数据 |
| 共享可变全局状态 | 测试 A 修改了全局变量,测试 B 受影响 | 使用 fixture,每次重新初始化 |
| 测试时间/随机数 | 断言依赖当前时间或随机数 | 注入时间/RNG 接口,测试时固定种子 |
| 过度 Mock | Mock 了所有依赖,测试变成了"验证 Mock 调用" | 优先使用真实实现或轻量 Fake |
| 过于脆弱的断言 | 断言准确的错误消息字符串 | 使用 ContainsSubstring 或自定义 Matcher |
浮点数直接 == |
REQUIRE(0.1 + 0.2 == 0.3) 会失败! |
使用 Approx、WithinAbs 或 WithinRel |
测试生产代码中的 assert() |
Release 编译 NDEBUG 下 assert 不生效 |
使用异常或返回错误码作为 API 契约 |
常见反面示例:
cpp
// ❌ 反模式:依赖测试顺序
static int counter = 0;
TEST_CASE("先执行") { counter = 42; }
TEST_CASE("后执行") { REQUIRE(counter == 42); } // 不可靠!
// ❌ 反模式:未隔离的共享资源
TEST_CASE("修改配置") {
Config::set("timeout", 30);
// ...
}
TEST_CASE("依赖配置") {
REQUIRE(Config::get("timeout") == 30); // 可能失败
}
// ✅ 正确做法:测试不共享可变状态
TEST_CASE("配置独立") {
auto config = ConfigManager(); // 每个测试独立创建
config.set("timeout", 30);
REQUIRE(config.get("timeout") == 30);
}
12. 参考资源
| 资源 | 链接 |
|---|---|
| Catch2 官方文档 | https://github.com/catchorg/Catch2/blob/devel/docs/Readme.md |
| Catch2 源码 | https://github.com/catchorg/Catch2 |
| CMake 集成指南 | https://github.com/catchorg/Catch2/blob/devel/docs/cmake-integration.md |
| Assertion 参考 | https://github.com/catchorg/Catch2/blob/devel/docs/assertions.md |
| Matchers 参考 | https://github.com/catchorg/Catch2/blob/devel/docs/matchers.md |
| Generators 参考 | https://github.com/catchorg/Catch2/blob/devel/docs/generators.md |
| FakeIt Mock 框架 | https://github.com/eranpeer/FakeIt |
| Trompeloeil Mock 框架 | https://github.com/rollbear/trompeloeil |
本指南使用 Mermaid 绘制架构流程图,在支持 Mermaid 渲染的 Markdown 编辑器(如 Typora、VS Code + Markdown Preview Mermaid Support)中可直接查看。
附录:快速参考卡片
断言宏速查
| 宏 | 含义 |
|---|---|
REQUIRE(expr) |
断言为真,失败即退出 |
CHECK(expr) |
断言为真,失败继续 |
REQUIRE_FALSE(expr) |
断言为假 |
REQUIRE_THAT(v, m) |
使用 Matcher 断言 |
REQUIRE_THROWS(expr) |
断言抛出任意异常 |
REQUIRE_THROWS_AS(expr, T) |
断言抛出 T 类型异常 |
REQUIRE_THROWS_WITH(expr, m) |
断言异常消息匹配 |
REQUIRE_NOTHROW(expr) |
断言不抛异常 |
BDD 宏速查
SCENARIO ≡ TEST_CASE(别名)
GIVEN(···) ≡ SECTION(···)
WHEN(···) ≡ SECTION(···)
THEN(···) ≡ SECTION(···)
AND_GIVEN ≡ SECTION(···)
AND_WHEN ≡ SECTION(···)
AND_THEN ≡ SECTION(···)
常用 Tags
[.] 隐藏测试(默认不运行,需主动指定)
[!throws] 标记可能崩溃的测试
[!mayfail] 标记已知失败的测试(失败不计入总数)
[!nonportable] 标记平台相关测试
[!benchmark] 👉 Catch2 支持简单的微基准测试
使用 [.] 隐藏测试的示例:
cpp
// 仅在明确指定时运行
TEST_CASE("慢速网络测试", "[network][.slow]") {
// ...
}
bash
./my_tests [.slow] # 运行被隐藏的慢速测试
./my_tests "[network]" # 默认不包含 [.slow] 标签的测试
./my_tests "[.slow]" # 仅运行隐藏的测试