5、断言的艺术:CUnit中的CU_ASSERT、CU_ASSERT_EQUAL、CU_ASSERT_STRING_EQUAL等断言详解
断言,说白了就是测试里的「裁判」。你写了一个函数,它该返回什么值?该有什么行为?断言就是帮你拍板的那个人。
我刚开始做嵌入式测试那会儿,总觉得断言就是比大小、比相等,没什么技术含量。直到有一次,一个字符串比较的bug让我排查了整整两天——嗯,从那以后我再也不敢小看断言了。
今天咱们就把CUnit里最常用的几个断言掰开揉碎,看看它们到底该怎么用、什么时候用、有什么坑。
5.1 断言家族概览
CUnit的断言其实分了好几类。我个人习惯把它们分成三组:
| 分类 | 典型断言 | 适用场景 |
|---|---|---|
| 基础断言 | CU_ASSERT(expr) | 布尔条件判断 |
| 数值断言 | CU_ASSERT_EQUAL(a, b) CU_ASSERT_NOT_EQUAL(a, b) CU_ASSERT_TRUE(expr) CU_ASSERT_FALSE(expr) |
整数、指针、枚举比较 |
| 字符串断言 | CU_ASSERT_STRING_EQUAL(s1, s2) CU_ASSERT_STRING_NOT_EQUAL(s1, s2) CU_ASSERT_NSTRING_EQUAL(s1, s2, n) |
字符串内容比较 |
你想想看,如果只用CU_ASSERT,其实也能干活。但为什么还要搞这么多花样的断言?因为——报错信息不一样。
核心原则:断言越具体,调试越省心。CU_ASSERT_EQUAL失败时会告诉你「期望是5,实际是3」,而CU_ASSERT只会说「条件为假」。你选哪个?
5.2 CU_ASSERT:最基础的裁判
CU_ASSERT(expr) 是所有断言的祖宗。它只做一件事:判断表达式是否为真。
// 示例:测试一个简单的校验函数
int is_valid_checksum(uint8_t *data, size_t len) {
uint8_t sum = 0;
for (size_t i = 0; i < len; i++) {
sum += data[i];
}
return (sum == 0);
}
void test_checksum_valid(void) {
uint8_t buf[] = {0x01, 0x02, 0x03, 0xFC}; // 0x01+0x02+0x03+0xFC = 0
CU_ASSERT(is_valid_checksum(buf, 4));
}
void test_checksum_invalid(void) {
uint8_t buf[] = {0x01, 0x02, 0x03, 0x04}; // 和不等于0
CU_ASSERT(!is_valid_checksum(buf, 4));
}
我在项目中遇到过一个问题:有人用CU_ASSERT(ptr != NULL)来检查指针,结果测试通过了,但实际运行时还是崩溃了。为什么?因为CU_ASSERT只检查表达式真假,它不会告诉你ptr的值到底是什么。后来我改成CU_ASSERT_PTR_NOT_NULL(ptr),报错信息直接打印出指针地址,排查起来快多了。
避坑指南:我曾经见过有人把CU_ASSERT当if用——在断言里写复杂的函数调用。比如CU_ASSERT(func_returning_side_effect() == 0)。这很危险!因为如果断言被禁用(比如发布版本),那个函数就不会被调用,副作用就没了。记住:断言里不要写有副作用的表达式。
5.3 CU_ASSERT_EQUAL:数值比较的利器
CU_ASSERT_EQUAL(expected, actual) 是我用得最多的断言。它比较两个值是否相等,支持整数、枚举、指针。
// 示例:测试一个温度转换函数
int celsius_to_fahrenheit(int celsius) {
return (celsius * 9 / 5) + 32;
}
void test_celsius_to_fahrenheit(void) {
CU_ASSERT_EQUAL(celsius_to_fahrenheit(0), 32);
CU_ASSERT_EQUAL(celsius_to_fahrenheit(100), 212);
CU_ASSERT_EQUAL(celsius_to_fahrenheit(-40), -40);
}
这里有个细节要注意:CU_ASSERT_EQUAL用的是==运算符。对于整数没问题,但如果是浮点数呢?
小技巧:浮点数比较不要用CU_ASSERT_EQUAL。因为浮点精度问题,0.1 + 0.2 可能不等于 0.3。我一般用CU_ASSERT_DOUBLE_EQUAL(actual, expected, delta),允许一个误差范围。比如CU_ASSERT_DOUBLE_EQUAL(0.1 + 0.2, 0.3, 0.0001)。
5.4 CU_ASSERT_STRING_EQUAL:字符串比较的陷阱
字符串比较是嵌入式测试里最容易出问题的地方。很多新手会用CU_ASSERT_EQUAL(str1, str2)来比较字符串——这其实是在比较指针地址,不是内容!
// 错误示范:比较指针地址
void test_wrong(void) {
char *s1 = "hello";
char *s2 = "hello";
CU_ASSERT_EQUAL(s1, s2); // 可能通过,也可能不通过!取决于编译器
}
// 正确做法:比较字符串内容
void test_correct(void) {
char buf[32];
strcpy(buf, "hello");
CU_ASSERT_STRING_EQUAL(buf, "hello"); // 比较内容
}
CU_ASSERT_STRING_EQUAL内部用的是strcmp。如果两个字符串相等,它返回0,断言通过。否则它会打印出两个字符串的内容,方便你对比。
我记得有一次,测试一个串口解析函数,总是断言失败。我盯着两个字符串看了半天,明明一模一样啊!后来才发现——一个字符串末尾有换行符\n,另一个没有。从那以后,我写字符串测试时都会先确认:有没有不可见字符?
// 示例:测试串口数据解析
void test_uart_parse(void) {
char input[] = "AT+OK\r\n";
char expected[] = "AT+OK";
char result[32];
parse_uart_data(input, result); // 去掉\r\n
CU_ASSERT_STRING_EQUAL(result, expected);
}
进阶用法:如果只比较前n个字符,用CU_ASSERT_NSTRING_EQUAL(s1, s2, n)。它内部用strncmp。比如你只关心字符串的前缀,或者字符串末尾有不确定的填充字符,这个就很有用。
5.5 其他实用断言
除了上面几个,CUnit还提供了不少「专病专药」的断言:
- CU_ASSERT_PTR_EQUAL(p1, p2):比较指针是否指向同一地址。比CU_ASSERT_EQUAL更语义化。
- CU_ASSERT_PTR_NULL(ptr):检查指针是否为NULL。比CU_ASSERT(ptr == NULL)报错信息更清晰。
- CU_ASSERT_PTR_NOT_NULL(ptr):检查指针非空。
- CU_ASSERT_BITS_EQUAL(mask, expected, actual):位比较。只关心某些位是否匹配。这个在寄存器测试里特别好用。
// 示例:寄存器位测试
void test_register_bits(void) {
uint32_t reg_value = 0xA5A5A5A5;
// 只检查bit0-bit3是否为0x5
CU_ASSERT_BITS_EQUAL(0x0F, 0x05, reg_value);
}
5.6 断言的艺术:什么时候用什么
说了这么多,到底该怎么选?我总结了一个简单的决策流程:
遇到一个测试点
|
├─ 是布尔条件? → CU_ASSERT(expr)
|
├─ 是数值比较?
| ├─ 整数/枚举 → CU_ASSERT_EQUAL
| ├─ 浮点数 → CU_ASSERT_DOUBLE_EQUAL
| └─ 位比较 → CU_ASSERT_BITS_EQUAL
|
├─ 是字符串?
| ├─ 完整比较 → CU_ASSERT_STRING_EQUAL
| ├─ 前缀比较 → CU_ASSERT_NSTRING_EQUAL
| └─ 忽略大小写 → 自己写个辅助函数
|
└─ 是指针?
├─ 判空 → CU_ASSERT_PTR_NULL / CU_ASSERT_PTR_NOT_NULL
└─ 判相等 → CU_ASSERT_PTR_EQUAL
我的习惯:能用具体断言就不用CU_ASSERT。因为具体断言失败时,CUnit会打印出「期望值」和「实际值」,而CU_ASSERT只会告诉你「第42行断言失败」。你想想看,哪个更容易定位问题?
5.7 本章小结
断言不是简单的「对或错」,它是你和代码之间的沟通桥梁。选对断言,能让你的测试代码更清晰,调试更高效。
最后送大家一句话:写断言时多花一分钟,调试时省下一小时。别嫌麻烦,这是过来人的经验。
公众号:蓝海资料掘金营,微信deep3321