3、单元测试框架:CUnit/Unity框架介绍、测试用例编写规范、Mock对象的使用
好,咱们进入第三章。这一章聊的是单元测试框架,具体说就是 CUnit 和 Unity 这两个家伙。我在扫地机项目里,这两套框架都用过,各有各的脾气。今天我把它们掰开揉碎了讲给你听。
3.1 为什么需要单元测试框架?
你可能会问:我直接写个 main 函数,调一下被测函数,打印结果看看不就行了?嗯,早期我也是这么干的。但项目一复杂,问题就来了——
- 测试代码散落各处,没法统一管理
- 没有断言机制,全靠肉眼盯着打印结果
- 没法自动化回归,改一次代码要手动跑半天
说白了,单元测试框架就是给你一套「标准化工具」。它帮你做三件事:组织测试用例、自动断言结果、生成测试报告。我个人的习惯是,项目启动第一天就把框架搭好,不然后面补测试,那叫一个痛苦。
3.2 CUnit 框架介绍
CUnit 是 C 语言里比较老牌的测试框架了。它长得有点像 JUnit,但毕竟是 C 语言,用起来更「原始」一些。
3.2.1 核心概念
- Test Registry(测试注册表):管理所有测试套件的地方
- Test Suite(测试套件):一组相关的测试用例集合
- Test Case(测试用例):最小的测试单元,就是一个函数
3.2.2 一个简单的例子
假设我们要测试扫地机的「电量计算函数」。代码长这样:
#include <CUnit/CUnit.h>
#include <CUnit/Basic.h>
// 被测函数
int calculate_battery_level(int voltage_mv) {
if (voltage_mv < 3000) return 0; // 没电了
if (voltage_mv > 4200) return 100; // 满电
return (voltage_mv - 3000) / 12; // 线性映射
}
// 测试用例
void test_battery_low(void) {
CU_ASSERT_EQUAL(calculate_battery_level(2500), 0);
}
void test_battery_full(void) {
CU_ASSERT_EQUAL(calculate_battery_level(4300), 100);
}
void test_battery_normal(void) {
CU_ASSERT_EQUAL(calculate_battery_level(3600), 50);
}
int main() {
CU_initialize_registry();
CU_pSuite suite = CU_add_suite("Battery Test Suite", NULL, NULL);
CU_add_test(suite, "test low battery", test_battery_low);
CU_add_test(suite, "test full battery", test_battery_full);
CU_add_test(suite, "test normal battery", test_battery_normal);
CU_basic_run_tests();
CU_cleanup_registry();
return 0;
}
运行后你会看到类似这样的输出:
Suite: Battery Test Suite
Test: test low battery ... passed
Test: test full battery ... passed
Test: test normal battery ... passed
--Run Summary: 3 tests, 3 passed, 0 failed--
3.3 Unity 框架介绍
Unity 是另一个轻量级框架,专门为嵌入式环境设计的。它比 CUnit 更「小巧」,没有动态内存分配,非常适合资源受限的 MCU 环境。我在扫地机的 STM32 项目里,用的就是 Unity。
3.3.1 Unity 的特点
- 全部用宏实现,没有运行时依赖
- 支持跨平台,可以在 PC 上先跑,再交叉编译到目标板
- 测试报告简洁,适合 CI 集成
3.3.2 同样的例子用 Unity 写
#include "unity.h"
int calculate_battery_level(int voltage_mv) {
if (voltage_mv < 3000) return 0;
if (voltage_mv > 4200) return 100;
return (voltage_mv - 3000) / 12;
}
void setUp(void) {
// 每个测试用例运行前执行,可以用来初始化
}
void tearDown(void) {
// 每个测试用例运行后执行,可以用来清理
}
void test_battery_low(void) {
TEST_ASSERT_EQUAL_INT(0, calculate_battery_level(2500));
}
void test_battery_full(void) {
TEST_ASSERT_EQUAL_INT(100, calculate_battery_level(4300));
}
void test_battery_normal(void) {
TEST_ASSERT_EQUAL_INT(50, calculate_battery_level(3600));
}
int main(void) {
UNITY_BEGIN();
RUN_TEST(test_battery_low);
RUN_TEST(test_battery_full);
RUN_TEST(test_battery_normal);
return UNITY_END();
}
| 对比项 | CUnit | Unity |
|---|---|---|
| 内存需求 | 较高(需要动态分配) | 极低(纯宏实现) |
| 适用平台 | PC/Linux 为主 | 嵌入式 MCU 首选 |
| 断言丰富度 | ★★★★ | ★★★★★ |
| 学习曲线 | 中等 | 平缓 |
我个人建议:如果你在 PC 上做单元测试,CUnit 够用;如果代码最终要跑在 MCU 上,直接用 Unity,省心。
3.4 测试用例编写规范
框架只是工具,真正重要的是怎么写好测试用例。我在项目中踩过不少坑,总结了几条铁律:
3.4.1 命名规范
- 测试函数名:test_模块名_功能名_场景名,例如 test_motor_speed_set_normal
- 测试文件:test_被测模块名.c,例如 test_battery.c
- 测试套件名:与模块名保持一致
3.4.2 结构规范
每个测试用例遵循 AAA 模式:
- Arrange(准备):初始化数据、设置 Mock 期望
- Act(执行):调用被测函数
- Assert(断言):验证结果是否符合预期
void test_sensor_read_valid(void) {
// Arrange
int expected_value = 25;
mock_adc_set_return(1, expected_value); // 模拟 ADC 通道1返回25
// Act
int actual = sensor_read_temperature();
// Assert
TEST_ASSERT_EQUAL_INT(expected_value, actual);
}
3.4.3 边界值测试
我曾经因为没测边界值,导致扫地机在电量 0% 时还强行启动电机,直接烧了驱动芯片。从那以后,边界值测试成了我的「必选项」。
3.5 Mock 对象的使用
嵌入式测试最头疼的是什么?依赖硬件!比如你要测试「根据传感器数据决定清扫路径」的函数,但传感器数据来自真实的 I2C 总线。这时候就需要 Mock 对象。
3.5.1 什么是 Mock?
说白了,Mock 就是「假模块」。它模拟真实模块的行为,让你能控制输入、验证输出。在扫地机项目里,我经常 Mock 这些:
- ADC 读取函数(返回固定电压值)
- 电机控制函数(记录调用次数和参数)
- 定时器回调(手动触发)
3.5.2 手动 Mock 示例
假设被测函数依赖一个外部函数 int get_battery_voltage(void),我们可以这样 Mock:
// 被测模块
int get_battery_percentage(void) {
int voltage = get_battery_voltage(); // 外部依赖
return calculate_battery_level(voltage);
}
// 测试文件中的 Mock
static int mock_voltage = 0;
int get_battery_voltage(void) {
return mock_voltage;
}
void test_battery_percentage_low(void) {
mock_voltage = 2500; // 设置 Mock 返回值
TEST_ASSERT_EQUAL_INT(0, get_battery_percentage());
}
void test_battery_percentage_full(void) {
mock_voltage = 4300;
TEST_ASSERT_EQUAL_INT(100, get_battery_percentage());
}
3.5.3 使用 CMock 自动生成
手动 Mock 写多了也烦。CMock 是 Unity 的搭档,它能根据头文件自动生成 Mock 代码。你只需要告诉它「我要 Mock 哪些函数」,剩下的它帮你搞定。
// 在测试配置中声明
// 假设 battery.h 中声明了 int get_battery_voltage(void);
// CMock 会自动生成 mock_battery.h 和 mock_battery.c
#include "mock_battery.h"
void test_battery_percentage(void) {
get_battery_voltage_ExpectAndReturn(2500); // 设置期望
TEST_ASSERT_EQUAL_INT(0, get_battery_percentage());
}
3.6 小结
这一章我们聊了 CUnit 和 Unity 两个框架,它们就像工具箱里的两把扳手——CUnit 适合 PC 端调试,Unity 更适合嵌入式环境。测试用例的编写规范,说白了就是「命名清晰、结构分明、边界覆盖」。而 Mock 对象,是解决硬件依赖的利器。
下一章,我们会把这些知识串起来,实际搭建一个扫地机模块的单元测试环境。到时候你会看到,这些框架和技巧是怎么在真实项目中落地的。