第三章 断言的艺术:CUnit常用断言宏、断言失败处理、自定义断言
断言这东西,说白了就是单元测试里的「裁判」。你写了一个函数,它该返回什么值、该有什么行为,断言就是帮你验证这些的。我刚开始写单元测试时,总觉得断言随便用用就行,后来踩了不少坑才明白——断言用得好不好,直接决定了你的测试代码是「靠谱的体检报告」还是「废纸一张」。
这一章,我们就来聊聊CUnit里的断言宏、失败时的处理策略,以及怎么写出自己的断言。
3.1 CUnit常用断言宏:你的测试工具箱
CUnit提供了丰富的断言宏,基本覆盖了日常需求。我个人习惯把它们分成三类:基础断言、比较断言、特殊断言。
3.1.1 基础断言
这类断言最简单,就是判断一个表达式是否为真或为假。
// 判断条件为真
CU_ASSERT(condition);
CU_ASSERT_TRUE(condition);
// 判断条件为假
CU_ASSERT_FALSE(condition);
嗯,这里要注意:CU_ASSERT和CU_ASSERT_TRUE功能一样,但后者可读性更好。我在项目中遇到过有人用CU_ASSERT(ptr != NULL),其实写成CU_ASSERT_TRUE(ptr != NULL)更直观。
3.1.2 比较断言
比较两个值是否相等、是否不等、谁大谁小。这是最常用的断言类型。
// 相等判断
CU_ASSERT_EQUAL(actual, expected);
CU_ASSERT_NOT_EQUAL(actual, expected);
// 数值比较
CU_ASSERT_LESS(actual, expected); // actual < expected
CU_ASSERT_GREATER(actual, expected); // actual > expected
CU_ASSERT_LESS_EQUAL(actual, expected);
CU_ASSERT_GREATER_EQUAL(actual, expected);
// 字符串比较
CU_ASSERT_STRING_EQUAL(str1, str2);
CU_ASSERT_STRING_NOT_EQUAL(str1, str2);
你想想看,这些宏的名字是不是很直观?EQUAL、LESS、GREATER,一眼就知道在比什么。我曾经在测试一个排序算法时,用CU_ASSERT_GREATER检查输出序列是否递增,省了不少事。
核心原则:断言宏的第一个参数永远是「实际值」,第二个是「期望值」。这个顺序别搞反了,否则报错信息会让你一头雾水。
3.1.3 特殊断言
有些场景需要更精细的控制,比如判断指针是否为空、判断数值是否在某个范围内。
// 指针判断
CU_ASSERT_PTR_NULL(ptr);
CU_ASSERT_PTR_NOT_NULL(ptr);
CU_ASSERT_PTR_EQUAL(ptr1, ptr2);
CU_ASSERT_PTR_NOT_EQUAL(ptr1, ptr2);
// 范围判断
CU_ASSERT_TRUE((value >= min) && (value <= max));
我记得有一次调试内存管理模块,用CU_ASSERT_PTR_NULL检查释放后的指针,一下子就抓到了野指针问题。这些宏虽然简单,但用对地方就是神器。
3.2 断言失败处理:优雅地面对失败
断言失败不代表世界末日,关键是怎么处理。CUnit提供了两种模式:致命断言和非致命断言。
3.2.1 致命断言 vs 非致命断言
| 类型 | 宏前缀 | 失败后行为 | 适用场景 |
|---|---|---|---|
| 非致命 | CU_ASSERT_* | 记录失败,继续执行 | 一般性检查,想看到所有失败点 |
| 致命 | CU_ASSERT_FATAL_* | 立即中止当前测试 | 前置条件不满足,后续测试无意义 |
举个例子:
void test_memory_operation(void) {
char *buffer = malloc(1024);
CU_ASSERT_FATAL_NOT_NULL(buffer); // 如果分配失败,没必要继续了
// 后续操作依赖buffer
memset(buffer, 0, 1024);
// ... 更多测试
free(buffer);
}
为什么会这样?因为如果malloc返回了NULL,后面的memset肯定崩溃。用致命断言可以避免这种连锁反应。我建议:前置条件用致命断言,功能验证用非致命断言。
3.2.2 自定义失败消息
CUnit允许你在断言失败时输出自定义信息,这对调试特别有帮助。
// 使用CU_ASSERT_MSG系列宏
CU_ASSERT_TRUE_MSG(condition, "自定义错误信息");
CU_ASSERT_EQUAL_MSG(actual, expected, "期望值=%d, 实际值=%d", expected, actual);
我曾经在测试一个通信协议解析器时,用CU_ASSERT_EQUAL_MSG打印出解析前后的数据包内容,一眼就看出是字节序处理错了。没有自定义消息的话,你只能看到「断言失败」,然后自己慢慢查。
小技巧:自定义消息里尽量包含上下文信息,比如变量名、期望值、实际值。这样失败时你不需要再翻代码。
3.3 自定义断言:打造你的专属武器
CUnit内置的断言虽然够用,但有些场景需要更高级的检查。比如,你想判断一个浮点数是否「接近」某个值,或者检查一个数组是否有序。这时候,自定义断言就派上用场了。
3.3.1 基于宏的自定义断言
最简单的做法是用宏封装现有断言:
#define ASSERT_FLOAT_NEAR(actual, expected, epsilon) \
CU_ASSERT_TRUE_MSG( \
fabs((actual) - (expected)) < (epsilon), \
"浮点数不接近: actual=%f, expected=%f, epsilon=%f", \
(actual), (expected), (epsilon) \
)
// 使用示例
void test_float_calculation(void) {
float result = 1.0f / 3.0f;
ASSERT_FLOAT_NEAR(result, 0.333333f, 0.0001f);
}
嗯,这里要注意:宏里的参数最好用括号包起来,避免运算符优先级问题。我刚开始写自定义断言时没注意这个,结果被坑过一次。
3.3.2 基于函数的自定义断言
如果逻辑比较复杂,用函数更合适:
// 检查数组是否有序(升序)
int assert_array_sorted(int *arr, int size) {
for (int i = 1; i < size; i++) {
if (arr[i-1] > arr[i]) {
CU_FAIL("数组未排序: arr[%d]=%d > arr[%d]=%d",
i-1, arr[i-1], i, arr[i]);
return 0;
}
}
return 1;
}
// 使用示例
void test_sort_function(void) {
int data[] = {1, 3, 2, 5, 4};
my_sort(data, 5);
assert_array_sorted(data, 5);
}
你想想看,如果每次都要手写循环检查数组,那得多累?封装成函数后,一行代码就能搞定。我在项目中维护过一个自定义断言库,里面大概有20多个这样的函数,覆盖了各种常见场景。
3.3.3 自定义断言的注意事项
- 保持一致性:自定义断言的命名风格要和CUnit保持一致,比如用
ASSERT_前缀。 - 提供清晰的错误信息:失败时一定要告诉用户「哪里错了」和「期望是什么」。
- 不要过度封装:如果逻辑太复杂,反而增加了维护成本。我见过有人把整个测试逻辑塞进一个宏里,结果出了问题根本不知道是测试代码错了还是被测代码错了。
避坑指南:我曾经写过一个自定义断言,里面调用了被测函数。结果被测函数有bug,导致断言本身崩溃了。从那以后,我规定自定义断言里绝对不能调用被测代码,只能做纯逻辑判断。
3.4 本章知识体系
下面这张图总结了断言的核心知识点,你可以把它当作速查表:
断言是单元测试的灵魂。用好内置宏、处理好失败场景、按需自定义,你的测试代码会越来越健壮。记住:好的断言,能让bug无处遁形;差的断言,只会让你怀疑人生。
公众号:蓝海资料掘金营,微信deep3321