STM32F4 Unity 单元测试与多次测试失败率统计详细步骤
本文档介绍如何在 STM32F4 工程中移植 Unity 单元测试框架,并实现多次循环测试、统计失败率的完整流程。
1. 概述
Unity 是一个轻量级的 C 语言单元测试框架,非常适合在 STM32 等嵌入式平台上运行。
本方案将 Unity 移植到 STM32F4,通过串口输出测试结果,并进一步实现:
- 多次重复运行所有测试用例
- 统计总失败次数、用例失败率、迭代失败率
- 通过全局变量控制是否输出详细测试过程,避免串口刷屏
2. 准备工作
2.1 硬件
- STM32F4 开发板(如 STM32F407 Discovery)
- ST-Link 调试器
- USB 转串口模块(或使用板载虚拟串口)
- 串口调试助手(如 SecureCRT、PuTTY、串口助手)
2.2 软件
- STM32CubeIDE / Keil MDK / IAR EWARM(任选其一)
- 一个可正常编译、下载、串口打印的 STM32F4 基础工程
- Unity 源码:从 GitHub 获取 ThrowTheSwitch/Unity
3. 添加 Unity 到工程
- 在工程根目录下新建
Unity文件夹。 - 将以下三个文件复制到
Unity文件夹:src/unity.csrc/unity.hsrc/unity_internals.h
- 在 IDE 中将
unity.c添加到编译列表,并将Unity文件夹加入头文件搜索路径。
推荐的文件结构示例:
YourProject/
├── Core/
│ ├── Inc/
│ │ ├── main.h
│ │ ├── unity_config.h
│ │ └── my_module.h
│ └── Src/
│ ├── main.c
│ ├── my_module.c
│ └── test_my_module.c
├── Unity/
│ ├── unity.c
│ ├── unity.h
│ └── unity_internals.h
└── ...
4. 配置 Unity
4.1 创建 unity_config.h
在 Core/Inc 下新建 unity_config.h,内容如下:
c
#ifndef UNITY_CONFIG_H
#define UNITY_CONFIG_H
#include "main.h" // 包含 HAL 库和 huart1 声明
extern UART_HandleTypeDef huart1;
extern int unity_verbose; // 全局变量,控制是否输出详细测试过程
// 将 Unity 的字符输出重定向到串口
#define UNITY_OUTPUT_CHAR(a) do { \
if (unity_verbose) { \
uint8_t ch = (uint8_t)(a); \
HAL_UART_Transmit(&huart1, &ch, 1, HAL_MAX_DELAY); \
} \
} while(0)
#endif
说明 :
unity_verbose为 0 时,Unity 内部的所有打印(测试名称、成功/失败信息)都会被丢弃,只保留我们自身通过uart_printf输出的统计信息。需要调试时将其置 1 即可看到详细过程。
4.2 让 Unity 识别配置文件
在 IDE 的编译器预定义宏中添加:
UNITY_INCLUDE_CONFIG_H
或者在 unity_internals.h 开头手动添加:
c
#define UNITY_INCLUDE_CONFIG_H
推荐使用编译器预定义方式,避免修改 Unity 源码。
4.3 实现 setUp 和 tearDown
Unity 要求必须定义这两个函数,即使为空。可以在 main.c 或单独的 unity_main.c 中实现:
c
#include "unity.h"
void setUp(void) {
// 每个测试用例执行前调用,可在此初始化变量、复位外设等
}
void tearDown(void) {
// 每个测试用例执行后调用,可在此进行清理
}
5. 编写测试用例
5.1 示例被测函数
创建 my_module.h 和 my_module.c:
my_module.h
c
#ifndef MY_MODULE_H
#define MY_MODULE_H
int AddFunction(int a, int b);
#endif
my_module.c
c
#include "my_module.h"
int AddFunction(int a, int b) {
return a + b;
}
5.2 创建测试文件
创建 test_my_module.c:
c
#include "unity.h"
#include "my_module.h"
void test_AddFunction_Should_Return_Sum(void) {
TEST_ASSERT_EQUAL_INT(5, AddFunction(2, 3));
TEST_ASSERT_EQUAL_INT(-1, AddFunction(2, -3));
}
void test_AddFunction_With_Zero(void) {
TEST_ASSERT_EQUAL_INT(0, AddFunction(0, 0));
TEST_ASSERT_EQUAL_INT(10, AddFunction(0, 10));
}
6. 单次测试运行(可选验证)
在 main.c 中先实现单次运行,确认 Unity 工作正常:
c
#include "main.h"
#include "unity.h"
void test_AddFunction_Should_Return_Sum(void);
void test_AddFunction_With_Zero(void);
int main(void) {
HAL_Init();
SystemClock_Config();
MX_USART1_UART_Init();
unity_verbose = 1; // 打开详细输出
UNITY_BEGIN();
RUN_TEST(test_AddFunction_Should_Return_Sum);
RUN_TEST(test_AddFunction_With_Zero);
UNITY_END();
while (1) {
}
}
烧录后打开串口助手(波特率与工程一致,如 115200),应能看到类似输出:
----------------------
2 Tests 0 Failures 0 Ignored
OK
7. 多次测试与失败率统计
7.1 设计思路
- 外层循环
TEST_ITERATIONS次。 - 每次循环调用
UnityBegin()重置 Unity 状态,运行所有测试,再调用UnityEnd()获取本轮失败数。 - 累加总失败次数、总执行用例次数、至少失败一次的迭代次数。
- 最后计算并输出失败率。
7.2 完整 main.c 示例
c
#include "main.h"
#include "unity.h"
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
extern UART_HandleTypeDef huart1;
int unity_verbose = 0; // 默认关闭 Unity 详细输出
// 自定义串口打印函数
void uart_printf(const char *fmt, ...) {
char buf[128];
va_list args;
va_start(args, fmt);
vsnprintf(buf, sizeof(buf), fmt, args);
va_end(args);
HAL_UART_Transmit(&huart1, (uint8_t*)buf, strlen(buf), HAL_MAX_DELAY);
}
// 测试用例声明
void test_AddFunction_Should_Return_Sum(void);
void test_AddFunction_With_Zero(void);
// 测试用例表
typedef struct {
void (*func)(void);
const char *name;
} test_case_t;
static test_case_t test_suite[] = {
{ test_AddFunction_Should_Return_Sum, "test_AddFunction_Should_Return_Sum" },
{ test_AddFunction_With_Zero, "test_AddFunction_With_Zero" },
};
#define NUM_TEST_CASES (sizeof(test_suite) / sizeof(test_suite[0]))
#define TEST_ITERATIONS 100
// 运行一轮所有测试
static void run_all_tests_once(void) {
for (size_t i = 0; i < NUM_TEST_CASES; i++) {
UnityDefaultTestRun(test_suite[i].func, test_suite[i].name, __LINE__);
}
}
int main(void) {
HAL_Init();
SystemClock_Config();
MX_USART1_UART_Init();
uart_printf("开始多次测试,迭代次数:%d\r\n", TEST_ITERATIONS);
int total_failures = 0;
int total_executions = 0;
int failed_iterations = 0;
for (int i = 0; i < TEST_ITERATIONS; i++) {
UnityBegin(__FILE__); // 重置并开始本轮
run_all_tests_once(); // 运行所有测试
int iter_failures = UnityEnd(); // 结束并获取本轮失败数
total_failures += iter_failures;
total_executions += NUM_TEST_CASES;
if (iter_failures > 0) {
failed_iterations++;
}
// 每轮简单输出(可选)
uart_printf("迭代 %3d: 失败 %d\r\n", i + 1, iter_failures);
}
// 最终统计
uart_printf("\r\n========== 测试统计 ==========\r\n");
uart_printf("总迭代次数 : %d\r\n", TEST_ITERATIONS);
uart_printf("总执行用例次数 : %d\r\n", total_executions);
uart_printf("总失败次数 : %d\r\n", total_failures);
uart_printf("用例失败率 : %.2f%%\r\n",
(float)total_failures / total_executions * 100.0f);
uart_printf("迭代失败率 : %.2f%%\r\n",
(float)failed_iterations / TEST_ITERATIONS * 100.0f);
while (1) {
}
}
7.3 输出结果示例
开始多次测试,迭代次数:100
迭代 1: 失败 0
迭代 2: 失败 0
...
迭代 99: 失败 1
迭代 100: 失败 0
========== 测试统计 ==========
总迭代次数 : 100
总执行用例次数 : 200
总失败次数 : 1
用例失败率 : 0.50%
迭代失败率 : 1.00%
8. 优化与注意事项
8.1 控制 Unity 输出
unity_verbose = 0:只输出自定义统计,适合批量测试。unity_verbose = 1:输出 Unity 详细过程,适合调试单个失败用例。- 若某轮出现失败,可记录轮次,之后单独将
unity_verbose置 1 并减少迭代次数,定位问题。
8.2 测试用例列表化
使用结构体数组管理测试用例,新增用例只需在 test_suite[] 中添加一行,无需修改循环逻辑。
8.3 硬件相关测试
- 如果测试涉及外设、中断、通信,建议在
setUp()中重新初始化相关外设。 - 每轮迭代之间可加
HAL_Delay(10)让系统稳定。 - 避免在测试中执行不可逆操作(如擦除 Flash)。
8.4 其他注意事项
- 栈大小 :
vsnprintf可能占用较多栈空间,若栈较小可减小buf或改用简单的字符串发送。 - 串口波特率:建议 115200 或更高,避免串口输出成为瓶颈。
- 迭代次数:根据测试耗时调整,避免测试时间过长。
- 浮点打印 :确保工程已启用浮点格式化支持(如
-u _printf_float)。 setUp/tearDown:必须定义,且要保证测试用例之间完全独立。
9. 进阶:PC 端与 CI
对于纯逻辑代码,更推荐在 PC 上使用 Ceedling 或 CMake + Unity 运行测试,速度快且易于集成 CI。STM32 上只保留少量硬件相关测试。
PC 端多次测试示例(Bash):
bash
for i in $(seq 1 100); do
./test_runner || echo "迭代 $i 失败" >> failures.log
done
10. 总结
通过以上步骤,你可以在 STM32F4 上:
- 成功移植 Unity 单元测试框架。
- 通过串口查看单次测试结果。
- 实现多次循环测试,并统计用例失败率和迭代失败率。
- 通过
unity_verbose灵活控制输出详细程度。 - 使用测试用例表方便扩展测试套件。
这套方案结构清晰、资源占用小,适合在嵌入式开发中持续验证代码逻辑。