第4章 单元测试基础:测试用例结构、断言宏的使用、测试夹具设计
好,咱们今天聊聊单元测试的骨架——测试用例怎么写、断言怎么用、夹具怎么搭。这三个东西,说白了就是测试代码的「砖瓦水泥」。我刚开始做嵌入式测试那会儿,觉得能跑通就行,后来被坑了几次才明白,结构不对,后面全是泪。
4.1 测试用例结构:别写成流水账
一个测试用例,本质上就是一个函数。但这个函数得有规矩。我个人习惯把测试用例分成三段:
- 准备阶段(Arrange):初始化变量、配置外设、设置预期值
- 执行阶段(Act):调用被测函数
- 验证阶段(Assert):检查结果是否符合预期
你想想看,如果这三段混在一起,代码读起来就像一团乱麻。我在项目中遇到过有人把断言写在初始化前面,结果跑了一晚上测试,第二天发现全是假阳性——因为变量还没赋值呢。
// 好的结构
void test_uart_send_byte(void)
{
// Arrange
uint8_t test_byte = 0xAB;
uart_config_t cfg = {.baudrate = 115200, .parity = UART_PARITY_NONE};
uart_init(&cfg);
// Act
uart_send_byte(test_byte);
// Assert
TEST_ASSERT_EQUAL_HEX8(test_byte, uart_get_tx_buffer_last());
}
4.2 断言宏的使用:别怕报错,怕的是不报错
断言是测试的眼睛。没有断言,测试就是瞎子。常用的断言宏就那么几个,但用好了能省大事。
| 断言宏 | 用途 | 我踩过的坑 |
|---|---|---|
| TEST_ASSERT_EQUAL | 比较两个值是否相等 | 浮点数别用这个,精度问题会让你怀疑人生 |
| TEST_ASSERT_TRUE/FALSE | 检查布尔条件 | 记得检查返回值,别直接传函数调用 |
| TEST_ASSERT_NULL/NOT_NULL | 检查指针 | 嵌入式里NULL指针太常见了,这个必须用 |
| TEST_ASSERT_EQUAL_HEX8/16/32 | 比较十六进制值 | 寄存器值比较时特别好用 |
嗯,这里要注意:断言失败后,测试框架会停止当前用例的执行。所以别把多个不相关的检查塞到一个用例里。我曾经犯过这个错——一个断言挂了,后面三个检查全跳过了,结果漏了一个严重的bug。
4.3 测试夹具设计:别重复造轮子
夹具(Fixture)是什么?说白了就是测试的「舞台布景」。每个测试用例都需要一个干净的环境,夹具就是帮你搭这个环境的。
我一般把夹具分成两类:
- Setup/Teardown:每个用例前后执行,保证环境隔离
- Suite Setup/Teardown:整个测试套件前后执行,适合初始化硬件
// 夹具示例
static uart_handle_t uart_handle;
void suite_setup(void)
{
// 整个测试套件只初始化一次
uart_hardware_init();
}
void setup(void)
{
// 每个用例前重置状态
uart_handle = uart_create_handle();
uart_reset_tx_buffer();
}
void teardown(void)
{
// 每个用例后清理
uart_destroy_handle(&uart_handle);
}
void test_uart_send_multiple_bytes(void)
{
uint8_t data[] = {0x01, 0x02, 0x03};
uart_send(data, 3);
TEST_ASSERT_EQUAL(3, uart_get_tx_count());
}
你想想看,如果没有夹具,每个用例里都得写一遍初始化代码,那得多烦?而且容易漏掉某个清理步骤,导致用例之间互相影响。我在项目中遇到过最离谱的一次——因为没清理全局变量,测试A写进去的数据被测试B读到了,两个测试居然都通过了,但实际产品一跑就崩。
核心原则:每个测试用例都应该能独立运行,且运行结果不依赖其他用例的执行顺序。夹具就是实现这个目标的工具。
4.4 实战中的那些坑
说了这么多理论,咱们聊聊实际干活时容易翻车的地方。
第一个坑:夹具太「重」
有些人喜欢在setup里初始化所有外设,结果一个测试套件跑下来,光初始化就花了半分钟。其实没必要,只初始化当前用例需要的东西就行。我一般会在setup里加个条件判断,按需初始化。
第二个坑:断言信息太少
你看这个断言:TEST_ASSERT_EQUAL(a, b)。如果失败了,你只知道a不等于b,但a是多少?b是多少?完全不知道。我建议用带消息的版本:TEST_ASSERT_EQUAL_MESSAGE(a, b, "UART baudrate mismatch")。这样失败时一眼就能看出问题在哪。
第三个坑:忽略边界条件
嵌入式系统里,边界条件特别多。比如定时器溢出、缓冲区满、队列空。我习惯在夹具里专门准备几组边界数据,每个用例都跑一遍。别嫌麻烦,这些地方最容易出bug。
好了,单元测试的基础就这些。结构清晰、断言到位、夹具干净,这三样做好了,你的测试代码就能跑得稳、查得快。下一章咱们聊聊怎么在嵌入式这种资源受限的环境里跑单元测试——嗯,那又是另一番天地了。