3. 搭建第一个单元测试环境:基于Unity的STM32项目实战
说实话,很多做单片机开发的兄弟,一听到「单元测试」就觉得是PC端那套东西,跟嵌入式不沾边。我以前也这么想,直到有一次在一个电机控制项目里,一个定时器溢出标志位的判断逻辑出了bug,硬是调了三天才找到。从那以后,我下定决心要把单元测试引入到STM32开发中来。
这一章,我们就拿Unity这个轻量级测试框架开刀,手把手在STM32上跑通第一个测试用例。Unity是啥?说白了就是一个纯C写的测试框架,就两个文件,不依赖任何操作系统,非常适合单片机这种资源受限的环境。
3.1 Unity框架的核心文件
Unity的源码结构极其简单,你下载下来会发现核心就三个文件:
| 文件 | 作用 | 我个人的使用习惯 |
|---|---|---|
unity.h |
主头文件,包含所有宏定义和函数声明 | 每个测试文件都必须包含它 |
unity.c |
核心实现,包含测试运行器和断言逻辑 | 直接加入工程编译,不需要改任何代码 |
unity_internals.h |
内部实现细节,一般不用管 | 除非你要定制输出格式,否则别碰它 |
test/unity/ 目录下,跟项目源码分开管理。
3.2 在STM32工程中集成Unity
嗯,这里要注意一个关键点:STM32的工程结构跟PC端不一样,我们不能直接拿IDE的默认工程来跑测试。我的做法是创建一个独立的测试工程,跟你的主工程共用源码,但链接的是测试入口。
具体步骤是这样的:
- 创建测试目录结构:在项目根目录下建一个
test/文件夹,里面放unity/和tests/两个子目录。 - 复制Unity源码:把
unity.h、unity.c、unity_internals.h放到test/unity/下。 - 编写测试入口:创建一个
test_main.c,里面包含main()函数,调用Unity的测试运行器。 - 配置编译器:在IDE中新建一个构建目标(Build Target),把测试目录加入头文件路径和源文件路径。
我习惯用STM32CubeIDE,它支持多构建配置。你可以在项目属性里新建一个叫 Debug_Test 的配置,专门用来编译测试代码。
3.3 编写第一个测试用例
假设我们要测试一个简单的计算函数——计算两个数的和。这个函数在 src/math_utils.c 里:
// src/math_utils.c
int add(int a, int b) {
return a + b;
}
对应的头文件 inc/math_utils.h:
// inc/math_utils.h
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
int add(int a, int b);
#endif
现在,我们在 test/tests/ 下创建测试文件 test_math_utils.c:
// test/tests/test_math_utils.c
#include "unity.h"
#include "math_utils.h"
// 每个测试用例前执行
void setUp(void) {
// 这里可以放初始化代码
}
// 每个测试用例后执行
void tearDown(void) {
// 这里可以放清理代码
}
// 测试用例:验证正数相加
void test_add_positive_numbers(void) {
TEST_ASSERT_EQUAL_INT(5, add(2, 3));
TEST_ASSERT_EQUAL_INT(0, add(0, 0));
TEST_ASSERT_EQUAL_INT(100, add(50, 50));
}
// 测试用例:验证负数相加
void test_add_negative_numbers(void) {
TEST_ASSERT_EQUAL_INT(-5, add(-2, -3));
TEST_ASSERT_EQUAL_INT(-1, add(2, -3));
}
// 测试用例:验证边界值
void test_add_boundary_values(void) {
TEST_ASSERT_EQUAL_INT(2147483647, add(2147483646, 1));
TEST_ASSERT_EQUAL_INT(-2147483648, add(-2147483647, -1));
}
3.4 测试运行器与主函数
有了测试用例,还需要一个入口来驱动它们。创建 test/test_main.c:
// test/test_main.c
#include "unity.h"
// 声明外部测试函数
void test_add_positive_numbers(void);
void test_add_negative_numbers(void);
void test_add_boundary_values(void);
int main(void) {
// 初始化Unity
UnityBegin("Math Utils Test Suite");
// 运行测试用例
RUN_TEST(test_add_positive_numbers);
RUN_TEST(test_add_negative_numbers);
RUN_TEST(test_add_boundary_values);
// 返回测试结果
return UnityEnd();
}
这里有个小细节:UnityBegin() 的参数是测试套件的名称,你可以随便起,但最好跟被测模块对应。我一般用「模块名 + Test Suite」的格式,方便后期查看报告。
3.5 编译与运行
在STM32CubeIDE中,按以下步骤操作:
- 右键项目 → Build Configurations → Set Active → 选择你创建的
Debug_Test配置。 - 点击编译按钮,确保没有编译错误。
- 连接开发板(比如STM32F4 Discovery),点击调试按钮。
- 在调试器的控制台窗口,你会看到Unity输出的测试结果。
输出大概长这样:
Math Utils Test Suite
---------------------
test_add_positive_numbers: PASS
test_add_negative_numbers: PASS
test_add_boundary_values: PASS
---------------------
3 Tests 0 Failures 0 Ignored
OK
看到这个「OK」,说明你的第一个单元测试环境已经跑通了。
3.6 测试结果的可视化
为了让你对整个测试流程有个直观的认识,我画了一张流程图:
3.7 常见问题与避坑指南
搭建过程中,你可能会遇到几个坑。我把自己踩过的列出来:
- 链接错误:找不到
main()—— 检查你的构建配置是否把test_main.c加入了编译,同时排除了原来的main.c。 - 断言宏不生效 —— 确认
unity.h是否在测试文件的开头被包含,而且没有被其他头文件的条件编译屏蔽。 - 测试结果全是FAIL —— 先检查
setUp()和tearDown()是否为空函数。如果里面写了硬件初始化代码,而测试环境没有对应硬件,就会出问题。
TEST_ASSERT_TRUE(1)。先确保整个链路通了,再逐步增加复杂用例。这样排查问题会快很多。
好了,到这里你已经成功搭建了第一个基于Unity的STM32单元测试环境。这个基础打牢了,后面讲测试覆盖率、持续集成、mock对象这些高级话题时,你才能跟得上。记住,测试不是写给别人看的,是写给自己用的——它能让你在凌晨三点改代码时,还有底气说一句「应该没问题」。