3. 搭建第一个单元测试环境:基于Unity的STM32项目实战

说实话,很多做单片机开发的兄弟,一听到「单元测试」就觉得是PC端那套东西,跟嵌入式不沾边。我以前也这么想,直到有一次在一个电机控制项目里,一个定时器溢出标志位的判断逻辑出了bug,硬是调了三天才找到。从那以后,我下定决心要把单元测试引入到STM32开发中来。

这一章,我们就拿Unity这个轻量级测试框架开刀,手把手在STM32上跑通第一个测试用例。Unity是啥?说白了就是一个纯C写的测试框架,就两个文件,不依赖任何操作系统,非常适合单片机这种资源受限的环境。

3.1 Unity框架的核心文件

Unity的源码结构极其简单,你下载下来会发现核心就三个文件:

文件 作用 我个人的使用习惯
unity.h 主头文件,包含所有宏定义和函数声明 每个测试文件都必须包含它
unity.c 核心实现,包含测试运行器和断言逻辑 直接加入工程编译,不需要改任何代码
unity_internals.h 内部实现细节,一般不用管 除非你要定制输出格式,否则别碰它
小提示:Unity的源码可以在GitHub上直接拉,版本选最新的稳定版就行。我建议你把这三个文件单独放在一个 test/unity/ 目录下,跟项目源码分开管理。

3.2 在STM32工程中集成Unity

嗯,这里要注意一个关键点:STM32的工程结构跟PC端不一样,我们不能直接拿IDE的默认工程来跑测试。我的做法是创建一个独立的测试工程,跟你的主工程共用源码,但链接的是测试入口。

具体步骤是这样的:

  1. 创建测试目录结构:在项目根目录下建一个 test/ 文件夹,里面放 unity/ 和 tests/ 两个子目录。
  2. 复制Unity源码:把 unity.h、unity.c、unity_internals.h 放到 test/unity/ 下。
  3. 编写测试入口:创建一个 test_main.c,里面包含 main() 函数,调用Unity的测试运行器。
  4. 配置编译器:在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));
}
注意:边界值测试在单片机中尤其重要。我曾经在一个工业控制项目里,因为没测INT_MAX的加法溢出,导致现场设备数据异常,差点造成停机事故。从那以后,边界值测试成了我每个测试文件的标配。

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中,按以下步骤操作:

  1. 右键项目 → Build Configurations → Set Active → 选择你创建的 Debug_Test 配置。
  2. 点击编译按钮,确保没有编译错误。
  3. 连接开发板(比如STM32F4 Discovery),点击调试按钮。
  4. 在调试器的控制台窗口,你会看到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 测试结果的可视化

为了让你对整个测试流程有个直观的认识,我画了一张流程图:

Unity 单元测试执行流程 被测源码 (math_utils.c) 测试用例 (test_math_utils.c) Unity 框架 (unity.c/h) 测试运行器 (test_main.c) 编译 & 链接 (STM32CubeIDE) 测试报告 (PASS/FAIL) 关键点 • 测试用例与源码分离 • Unity框架无平台依赖 • 测试运行器是唯一入口

3.7 常见问题与避坑指南

搭建过程中,你可能会遇到几个坑。我把自己踩过的列出来:

  • 链接错误:找不到 main() —— 检查你的构建配置是否把 test_main.c 加入了编译,同时排除了原来的 main.c。
  • 断言宏不生效 —— 确认 unity.h 是否在测试文件的开头被包含,而且没有被其他头文件的条件编译屏蔽。
  • 测试结果全是FAIL —— 先检查 setUp() 和 tearDown() 是否为空函数。如果里面写了硬件初始化代码,而测试环境没有对应硬件,就会出问题。
我的经验:第一次跑测试时,建议只写一个最简单的测试用例,比如 TEST_ASSERT_TRUE(1)。先确保整个链路通了,再逐步增加复杂用例。这样排查问题会快很多。

好了,到这里你已经成功搭建了第一个基于Unity的STM32单元测试环境。这个基础打牢了,后面讲测试覆盖率、持续集成、mock对象这些高级话题时,你才能跟得上。记住,测试不是写给别人看的,是写给自己用的——它能让你在凌晨三点改代码时,还有底气说一句「应该没问题」。