STM32F4 Unity 单元测试与多次测试失败率统计详细步骤

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 到工程

  1. 在工程根目录下新建 Unity 文件夹。
  2. 将以下三个文件复制到 Unity 文件夹:
    • src/unity.c
    • src/unity.h
    • src/unity_internals.h
  3. 在 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 上:

  1. 成功移植 Unity 单元测试框架。
  2. 通过串口查看单次测试结果。
  3. 实现多次循环测试,并统计用例失败率和迭代失败率。
  4. 通过 unity_verbose 灵活控制输出详细程度。
  5. 使用测试用例表方便扩展测试套件。

这套方案结构清晰、资源占用小,适合在嵌入式开发中持续验证代码逻辑。

相关推荐
Brilliantwxx2 小时前
【STM32】 I2C 中断模式收发数据实战:从阻塞到中断的架构改造(AT24C02)
stm32·单片机·嵌入式硬件
星河单片机4 小时前
单片机成品方案:基于STM32单片机智能门禁系统
stm32·单片机·嵌入式硬件
en.en..5 小时前
STM 与 ARM
stm32
Naisu Xu6 小时前
记一次STM32H503冷启动后进入HardFult_Handler的问题
stm32·单片机·嵌入式硬件·调试·内存问题
淡海水6 小时前
16-01-C#数据结构源码-附录A-术语表
unity·c#·游戏引擎·il2cpp
程序员正茂7 小时前
Keil中将device由 STM32F103ZE 改为 STM32F103C8
stm32·单片机·嵌入式硬件
LCG元7 小时前
STM32实战:基于STM32F103的智能路灯系统(光敏检测+人体感应+自动照明)
stm32·单片机·嵌入式硬件
淡海水7 小时前
16-03-C#数据结构源码-附录C-参考资源
unity·c#·游戏引擎·il2cpp
LCG元8 小时前
STM32F103 I2C 驱动 BMP280 气压温度传感器实战:校准补偿公式拆解、IIR 滤波整定与海拔换算实测
stm32·单片机·嵌入式硬件