培育能力的事必须继续不断地去做,又必须随时改善学习方法,提高学习效率,才会成功。——叶圣陶

https://github.com/catchorg/Catch2

Catch2:让 C++ 测试写起来更像在写 C++ 本身

测试框架这件事,说到底是为了帮开发者把“我想验证什么”表达得更自然。如果一个测试框架本身写起来就拧巴,那么测试代码很快也会变成另一种负担。Catch2 给人的第一印象,恰恰是它想把这件事变简单。

根据仓库 description,Catch2 是一个现代的、C++ 原生的测试框架,面向 unit-tests、TDD 和 BDD,支持 C++14、C++17 以及更高版本。README 则进一步说明,Catch2 主要是一个 C++ 单元测试框架,同时也提供 基础的微基准测试能力,以及 简单的 BDD 宏

这是一种很清晰的定位:它首先是测试框架,但又不只停留在最基础的断言层面。它希望把单元测试、测试驱动开发、行为驱动风格,以及一部分性能测量能力,尽量放进同一个自然的 C++ 语境里。

项目地址:https://github.com/catchorg/Catch2

Catch2 想解决的,不只是“能不能测”,而是“写起来顺不顺”

README 对 Catch2 的优势总结得非常直接:使用起来简单而自然

它提到了几个很能说明气质的点:

  • 测试名称不必是合法标识符
  • 断言看起来像普通的 C++ 布尔表达式
  • sections 提供了一种优雅而局部的方式共享测试中的 set-up 和 tear-down 代码

这些话放在一起,其实已经很接近 Catch2 的核心魅力了。

很多测试框架的使用成本,不一定来自功能缺失,而是来自表达方式太生硬。测试名字受限、断言语法绕、公共准备代码散得到处都是,最后明明只是想验证一个函数,却像在和框架语法较劲。Catch2 明显不想让这种情况发生,它更希望测试代码读起来像是在正常地写 C++,而不是先学一层额外腔调。

一个简单的单元测试例子,就能看出它的风格

README 给出了一段非常经典的示例:用阶乘函数来写单元测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <catch2/catch_test_macros.hpp>

#include <cstdint>

uint32_t factorial( uint32_t number ) {
return number <= 1 ? number : factorial(number-1) * number;
}

TEST_CASE( "Factorials are computed", "[factorial]" ) {
REQUIRE( factorial( 1) == 1 );
REQUIRE( factorial( 2) == 2 );
REQUIRE( factorial( 3) == 6 );
REQUIRE( factorial(10) == 3'628'800 );
}

这段代码之所以好懂,不是因为示例足够简单,而是因为 Catch2 的测试写法本身就很直白。

TEST_CASE 直接表达测试主题,REQUIRE 则像在大声说:“这里必须成立。”
你不需要绕进复杂的断言结构里,也不需要给测试名强行套进某种僵硬标识符规范中。测试标题可以是自然语言,断言也几乎就是普通布尔判断的形状。

对于 C++ 开发者来说,这种“顺着语言本身来”的感觉,往往比功能表里多几个开关更重要。

它不只测正确性,也愿意碰一点性能

Catch2 的 README 明确说明,它还提供 basic micro-benchmarking features。这意味着它不仅关心“结果是不是对”,也给了开发者一个轻量入口,去观察某段代码在微观层面的运行表现。

README 中的 Fibonacci 示例就把这一点表现得很直接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <catch2/catch_test_macros.hpp>
#include <catch2/benchmark/catch_benchmark.hpp>

#include <cstdint>

uint64_t fibonacci(uint64_t number) {
return number < 2 ? number : fibonacci(number - 1) + fibonacci(number - 2);
}

TEST_CASE("Benchmark Fibonacci", "[!benchmark]") {
REQUIRE(fibonacci(5) == 5);

REQUIRE(fibonacci(20) == 6'765);
BENCHMARK("fibonacci 20") {
return fibonacci(20);
};

REQUIRE(fibonacci(25) == 75'025);
BENCHMARK("fibonacci 25") {
return fibonacci(25);
};
}

这个示例很有意思,因为它没有把 benchmark 和 correctness 完全割裂开。
它先用 REQUIRE 验证结果,再用 BENCHMARK 对指定计算做微基准测试。这样的写法让“先确认逻辑,再观察表现”变成一条很顺的路径。

README 还特别提醒:benchmark 默认不会运行,需要通过 [!benchmark] 标签显式执行。

这个细节也很重要。它说明 Catch2 对测试场景是有区分的,不会把基准测试默认塞进每次常规执行里,而是让它以更明确的方式存在。

BDD 不是口号,README 里已经把它放进定位中

仓库 description 中明确提到 Catch2 面向 TDD 和 BDD,README 也说它提供 simple BDD macros

虽然当前 README 首页没有展开 BDD 宏的具体语法,但仅从定位上看,Catch2 并没有把自己局限在“只会写普通断言式单元测试”的框架形象里。它承认开发者在测试表达上会有不同偏好,也把行为驱动风格纳入了自身能力范围。

这使得 Catch2 的测试表达空间更宽一些。
有的人喜欢简洁、直接、函数级的断言;有的人更希望测试描述和行为语义更强。Catch2 在项目介绍中已经表明,它愿意为这两种使用习惯同时腾位置。

Catch2 v3 的变化,是一次很重要的转身

README 里有一段很关键的信息:Catch2 v3 has been released

而且 README 明确说明,你当前所在的是 devel 分支,这里是 v3 的开发分支。仓库 metadata 也显示,这个仓库的默认分支正是 devel

更重要的是,README 把 v3 的一个大变化说得非常直白:

Catch2 is no longer a single-header library.
Catch2 now behaves as a normal library, with multiple headers and separately compiled implementation.

这是个相当鲜明的转折点。

过去很多人提到 Catch,往往会立刻想到“单头文件测试框架”这种印象。但在 v3 里,README 已经明确表示,Catch2 不再是单头文件库,而是更像一个普通库那样工作:有多个头文件,也有单独编译的实现部分。

这不是一句轻描淡写的更新说明,而是一次对使用形态的重新界定。它意味着 Catch2 在架构和集成方式上,已经与早期的“单头文件”印象拉开了距离。

README 还提到,文档正在逐步更新以适应这些变化,这项工作仍在进行中。
同时,对于从 v2 迁移到 v3 的用户,README 也明确给出方向:可以查看迁移文档,其中包含上手指引和常见迁移问题汇总。

另外,如果你需要之前的大版本,README 也指出可以查看 GitHub 上的 v2.x 分支。

这一段信息整体上给人的感觉很坦率:版本确实发生了大变化,项目也没有试图掩饰这件事,而是把变化、迁移方向和旧版本入口都摆在前面。

“自然”这件事,在测试名字和断言里体现得很明显

README 里有一句很值得反复品味的话:test names do not have to be valid identifiers

这件事看似不大,实际上非常影响测试可读性。

因为一旦测试名不再被“变量命名规则”束缚,测试代码就更容易直接表达意图。你可以把测试标题写成真正面向人的语句,而不是被迫挤成某种技术化缩写。测试一旦可读,就更容易成为文档的一部分,而不只是执行器勉强能识别的符号集合。

再加上 README 提到的另一点:assertions look like normal C++ boolean expressions
这意味着 Catch2 在语法层面努力减少“框架腔”,让你更专注于条件本身,而不是测试 DSL 的绕法。

这种体验上的减负,往往是一个测试框架能不能长期留下来的关键。

sections 这一点,也很像是为真实测试场景准备的

README 提到,sections provide a nice and local way to share set-up and tear-down code in tests

虽然首页没有展开具体代码示例,但这句话已经传递出一个很清楚的方向:Catch2 关心的不只是“能不能断言”,也关心测试组织结构是否容易维护。

在实际测试中,最容易让人头疼的往往不是断言本身,而是准备逻辑和清理逻辑如何安排。如果共享代码写得太远,测试就会失去局部性;如果每段测试都完全重复,又很快会变得笨重。README 把 sections 单独拿出来说,说明它在 Catch2 里的位置并不边缘,而是“自然测试组织方式”的一部分。

Catch2 的文档结构很朴素,但方向很明确

README 在 “How to use it” 一节里,把文档分成了三部分:

  • 为什么还需要另一个 C++ 测试框架
  • Tutorial
  • Reference section

这种结构其实很讨喜,因为它分别对应三种完全不同的阅读需求:

  • 想先理解理念的人
  • 想快速开始的人
  • 想查细节的人

一个测试框架的文档如果只有 API 列表,通常不够友好;如果只有宣传性介绍,也不够落地。Catch2 在 README 里至少把入口分层做得很清楚,让人知道自己该从哪儿开始。

从仓库 description 看,它的语言边界也讲得很明白

仓库 description 里除了说它是现代的 C++ 原生测试框架,还明确给出了标准支持范围:

  • 使用 C++14、C++17 及更高版本
  • C++11 支持在 v2.x 分支
  • C++03 支持在 Catch1.x 分支

这段信息很有分寸感。它没有模糊地说“支持各种 C++ 版本”,而是把不同标准支持与不同分支的关系说清楚。这样一来,读者能更明确地知道自己应该关注哪个版本线。

同时,这种说明也和 README 中关于 v3、v2.x 的版本分层形成了呼应:Catch2 不是把所有历史兼容性都混成一团,而是通过不同分支来承载不同阶段的能力边界。

它的身份非常纯粹:测试框架,但不是单薄的测试框架

如果只看第一眼,Catch2 像是一个“写单元测试的库”。
但 README 和 description 拼在一起后,会发现它的定位其实更完整一些:

  • 它是 C++ 的单元测试框架
  • 它面向 TDD
  • 它支持简单的 BDD 宏
  • 它提供基础的微基准测试能力
  • 它强调测试写法的自然性
  • 它通过 sections 改善测试中的局部组织方式

这种组合很有意思。它没有把自己包装成包治百病的大而全平台,而是在测试这条主线上,尽可能把几类高频需求组织到一起:正确性验证、行为表达、性能观察、测试结构组织。

这会让它显得“专注”,但并不“单薄”。

默认分支是 devel,这件事本身也透露出项目节奏

根据仓库信息,Catch2 的默认分支是 devel。README 也明确写着:你所在的是 devel 分支,v3 正在这里开发。

这会带来一种很直观的项目印象:Catch2 的主仓库展示的是一个持续演进中的主线,而不是把默认入口固定在某个只讲历史的稳定快照上。README 也坦诚地提到,文档还在慢慢更新以覆盖 v3 带来的变化。

这种表达方式并不花哨,但很真实。它像是在告诉使用者:这个项目在往前走,而且它愿意把变化直接展示出来。

从语言构成和仓库信息看,它有非常明确的“C++ 原生气质”

仓库信息显示,Catch2 的主要语言是 C++
这一点几乎不用多解释,因为整个项目介绍都围绕 “C++-native” 展开。

许可证方面,仓库使用的是 Boost Software License 1.0。默认分支为 devel,项目长期围绕 C++ 测试框架这一核心方向展开。再加上 README 中的示例代码和文档组织方式,这个仓库给人的整体印象很一致:没有刻意绕去别处,它就是认真在做 C++ 测试体验这件事。

如果把 Catch2 拟人化一点来看

Catch2 很像那种说话不多,但特别懂开发者别扭点的工具。
它不会先端上一套冗长仪式,而是先告诉你:“来,直接写测试吧,而且尽量按你熟悉的 C++ 方式写。”
它不强迫测试名变成拧巴的标识符,不把断言弄成另一门小语言,也不把微基准测试排除在门外。它像是在测试代码和 C++ 代码之间,悄悄拆掉了几堵本来不必要的墙。

结语

Catch2 的吸引力,不在于它把自己说得多复杂,而在于它把“测试应该自然”这件事贯彻得很明确。README 用很短的篇幅就讲清了它的几个核心点:单元测试是主场,微基准测试和简单 BDD 宏是补充;测试名称更自由,断言更像普通 C++ 表达式,sections 则帮助组织测试结构。

而 v3 的发布,又让这个项目从“单头文件时代的经典印象”继续迈向更像常规库的使用形态。它在变化,但仍然保留着最让人记住的那个气质:让 C++ 测试尽量回到 C++ 本身的语言感觉里。