第四章:断言的艺术——EXPECT_TRUE、ASSERT_EQ、EXPECT_THROW 等断言宏的深入理解与使用场景
断言,说白了就是测试的「灵魂」。没有断言,测试跑得再欢也是白搭。我见过不少团队,测试代码写了一堆,结果全是 std::cout 打印日志,然后人眼去比对——这哪叫测试?这叫自欺欺人。
Google Test 提供了两大家族断言:ASSERT_* 和 EXPECT_*。它们长得像,脾气可不一样。今天咱们就把它们掰开揉碎了讲清楚。
4.1 致命与温和:ASSERT 与 EXPECT 的核心区别
先记住一句话:ASSERT 是「一票否决」,EXPECT 是「记过处分」。
- ASSERT_*:如果失败,立即终止当前测试函数。后面的代码不跑了。
- EXPECT_*:如果失败,记录失败,但继续执行。测试函数会跑完,最后报告所有失败。
我早期做项目时,有个同事在初始化阶段用了 EXPECT_TRUE 检查资源是否加载成功。结果后面所有操作都基于这个资源,一路崩溃到底,一个测试报了 47 个错误。排查起来简直噩梦。后来改成 ASSERT_TRUE,第一个失败就停,问题立刻定位。
核心原则:
- 如果后续代码依赖当前断言的结果,用
ASSERT_* - 如果只是检查某个属性,不影响后续逻辑,用
EXPECT_*
4.2 布尔断言:EXPECT_TRUE 与 EXPECT_FALSE
这是最基础的断言,但坑也不少。
// 基础用法
EXPECT_TRUE(ptr != nullptr);
EXPECT_FALSE(list.empty());
// 注意:不要写成 EXPECT_TRUE(ptr) 这种隐式转换
// 虽然能编译,但可读性差,而且可能隐藏类型转换问题
我个人习惯:永远写完整的布尔表达式。你想想看,EXPECT_TRUE(ptr) 和 EXPECT_TRUE(ptr != nullptr),后者一眼就能看出你在检查空指针。代码是写给下一个维护者看的,包括三个月后的你自己。
小技巧:对于指针检查,我更喜欢用 ASSERT_NE(ptr, nullptr),语义更清晰。Google Test 的断言宏覆盖了几乎所有比较操作,没必要死磕布尔版本。
4.3 比较断言:ASSERT_EQ、ASSERT_NE、ASSERT_LT 等
这些是日常使用频率最高的断言。但有个大坑——浮点数比较。
// 整数比较,没问题
ASSERT_EQ(42, compute_answer());
// 浮点数比较,大坑!
// ASSERT_EQ(3.14, compute_pi()); // 几乎必然失败
// 正确做法
ASSERT_FLOAT_EQ(3.14f, compute_pi_float());
ASSERT_DOUBLE_EQ(3.1415926, compute_pi_double());
// 或者自定义精度
EXPECT_NEAR(3.14159, compute_pi(), 1e-5);
我曾经接手过一个遗留系统,里面全是 ASSERT_EQ 比较浮点数。跑测试时,同样的代码在 CI 上全绿,本地却一片红。折腾了两天,最后发现是编译器优化级别不同导致浮点精度差异。从那以后,我定了个规矩:浮点数比较,必须用 ASSERT_NEAR 或 ASSERT_DOUBLE_EQ。
| 断言宏 | 用途 | 注意事项 |
|---|---|---|
ASSERT_EQ |
相等比较 | 浮点数慎用 |
ASSERT_NE |
不等比较 | 指针比较时注意 nullptr |
ASSERT_LT |
小于 | 注意边界值 |
ASSERT_LE |
小于等于 | 常用于循环边界检查 |
ASSERT_GT |
大于 | 常用于有效性检查 |
ASSERT_GE |
大于等于 | 同上 |
4.4 字符串断言:EXPECT_STREQ 与 EXPECT_STRNE
C 风格字符串和 C++ std::string 的处理方式完全不同,这里最容易翻车。
std::string cpp_str = "hello";
const char* c_str = "hello";
// 正确:比较 C++ 字符串
EXPECT_EQ(cpp_str, "hello");
// 正确:比较 C 风格字符串
EXPECT_STREQ(c_str, "hello");
// 错误!这会比较指针地址,而不是内容
// EXPECT_EQ(c_str, "hello"); // 几乎必然失败
嗯,这里要注意:EXPECT_EQ 对于 const char* 会退化为指针比较。我见过有人因为这个 bug 排查了一整天——测试明明应该通过,却总是失败。最后发现是字符串字面量的地址不同。
避坑指南:如果你在写 C++ 代码,尽量用 std::string。这样直接用 EXPECT_EQ 就行,省心。如果必须用 C 风格字符串,记得用 EXPECT_STREQ。
4.5 异常断言:EXPECT_THROW、EXPECT_NO_THROW 与 EXPECT_ANY_THROW
异常测试是很多人容易忽略的部分。你想想看,一个函数说「参数非法时抛出异常」,你怎么验证它真的抛了?
// 期望抛出特定异常
EXPECT_THROW({
Database db;
db.connect("invalid_url");
}, ConnectionException);
// 期望不抛出任何异常
EXPECT_NO_THROW({
Database db;
db.connect("valid_url");
});
// 期望抛出任意异常(不推荐,太宽泛)
EXPECT_ANY_THROW({
risky_operation();
});
我个人习惯:尽量用 EXPECT_THROW 指定具体异常类型。为什么?因为 EXPECT_ANY_THROW 太宽松了,万一函数因为其他 bug 抛了个 std::bad_alloc,测试也通过了,这就掩盖了真正的问题。
我曾经在一个支付系统里,有个函数应该抛出 InsufficientFunds 异常。测试用了 EXPECT_ANY_THROW,结果某次重构后,函数因为空指针崩溃了,抛的是 std::runtime_error。测试依然通过,bug 直接上了生产环境。教训惨痛。
4.6 断言宏的选择策略
说了这么多,到底怎么选?我总结了一个决策流程:
// 伪代码:断言选择策略
if (后续代码依赖此结果) {
if (检查布尔条件) → ASSERT_TRUE / ASSERT_FALSE
if (比较值) → ASSERT_EQ / ASSERT_NE / ...
if (检查异常) → ASSERT_THROW / ASSERT_NO_THROW
} else {
if (检查布尔条件) → EXPECT_TRUE / EXPECT_FALSE
if (比较值) → EXPECT_EQ / EXPECT_NE / ...
if (检查异常) → EXPECT_THROW / EXPECT_NO_THROW
}
我的个人经验法则:
- 测试函数的前半部分用
ASSERT_*做前置条件检查 - 后半部分用
EXPECT_*做结果验证 - 异常测试永远用
EXPECT_THROW指定具体类型 - 浮点数比较永远用
ASSERT_NEAR或ASSERT_DOUBLE_EQ
4.7 本章知识体系
下面这张图总结了断言宏的完整分类和使用场景:
4.8 实战建议
最后,分享几个我多年积累的实战经验:
- 测试函数不要写太长。一个测试函数只测一个行为。如果断言太多,说明测试粒度太粗。
- 断言信息要可读。Google Test 支持自定义失败信息:
ASSERT_EQ(a, b) << "a 的值是 " << a << ", b 的值是 " << b;。别偷懒,加上去。 - 不要滥用 ASSERT。我见过有人一个测试函数里写了 10 个 ASSERT,第一个失败后面全跳过。这其实丢掉了大量信息。能用 EXPECT 的地方尽量用 EXPECT。
- 异常测试要覆盖 happy path 和 sad path。不仅要测「应该抛异常时抛了」,还要测「不该抛异常时没抛」。
最后一个小秘密:Google Test 的断言宏本质上是宏,不是函数。这意味着你可以在断言里写复杂的表达式,但要注意宏展开可能带来的副作用。比如 ASSERT_EQ(++x, 5) 这种代码,千万别写——你永远不知道宏展开几次。
断言是测试的「眼睛」。用好它们,你的测试才能真正帮你发现问题,而不是制造噪音。下一章,咱们聊聊测试夹具(Test Fixture)——怎么优雅地复用测试代码。
公众号:蓝海资料掘金营,微信deep3321