第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。

我的习惯:每个模块的测试夹具都单独写一个.c文件,和测试用例分开。这样夹具可以复用,而且修改夹具时不会影响到用例代码。

好了,单元测试的基础就这些。结构清晰、断言到位、夹具干净,这三样做好了,你的测试代码就能跑得稳、查得快。下一章咱们聊聊怎么在嵌入式这种资源受限的环境里跑单元测试——嗯,那又是另一番天地了。