Catch2 单元测试指南

核心目标:掌握 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);
            }
        }
    }
}

SCENARIOTEST_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) 会失败! 使用 ApproxWithinAbsWithinRel
测试生产代码中的 assert() Release 编译 NDEBUGassert 不生效 使用异常或返回错误码作为 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 宏速查

SCENARIOTEST_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]"      # 仅运行隐藏的测试
相关推荐
诸葛李9 小时前
集成构建xxxxx
java·junit·单元测试
yeshan1 天前
【Draft】基于 cluacov 的 Lua 代码分支覆盖率统计:从行级近似到指令级精确
单元测试·lua
姚青&1 天前
测试体系与测试方案设计
单元测试
QH139292318803 天前
思仪 Ceyear 5256C 5G 终端综合测试仪
单片机·单元测试·集成测试·嵌入式实时数据库
汽车仪器仪表相关领域4 天前
Debron OVM 1052 光学关门速度仪:汽车门盖检测的高精度便携工具 + 生产线适配 + 耐久性监测,整车制造与质量控制的黄金标准
人工智能·功能测试·单元测试·汽车·制造·可用性测试
Sandy_Star5 天前
1.9 民法典及社会保险法制度规定
单元测试
Sandy_Star5 天前
1.7 税务行政法律救济
大数据·单元测试
Kiyra6 天前
Query Rewrite 不是越智能越好:RAG 检索的精确词保护与动态召回
redis·websocket·junit·单元测试·json