嵌入式单元测试:Unity/CMock框架与硬件在环测试——测试驱动、桩函数
文章目录

每日一句正能量
筛选比改变更重要。
成年人的关系和时间都很珍贵。试图改变别人往往徒劳且消耗自己,而懂得筛选对的人、对的事,则是一种更高效、更清醒的生活策略。
一、前言:为什么嵌入式软件必须做单元测试?
在嵌入式开发领域,"代码能跑就行"的时代早已过去。随着物联网、智能汽车、工业控制等场景的复杂度指数级增长,嵌入式固件的可靠性和可维护性已成为产品成败的关键。据统计,缺陷修复成本在编码阶段仅为1倍,到系统集成阶段飙升至10倍,而部署到现场后可达100倍以上。对于运行在资源受限MCU上的固件,一旦出现问题,OTA升级困难、现场调试成本高,甚至涉及人身安全。
传统的嵌入式测试往往依赖"printf调试"和"板上验证",这种方式存在三大痛点:
- 硬件依赖性强:没有开发板就无法测试,团队成员需要排队等待硬件资源
- 反馈周期长:修改一行代码→编译→烧录→运行→观察,循环动辄数分钟
- 边界条件难覆盖:极端温度、传感器故障、通信超时等场景难以在真实环境中复现
单元测试(Unit Testing) 正是解决这些痛点的银弹。通过在PC主机上模拟目标环境,开发者可以在秒级获得测试反馈,自动化覆盖正常路径与异常分支。本文将深入讲解嵌入式C语言单元测试的黄金组合——Unity + CMock + Ceedling,并结合**硬件在环测试(HIL)**构建从桌面到板级的完整测试体系。
二、测试工具链全景:Unity / CMock / Ceedling
嵌入式C语言单元测试领域,ThrowTheSwitch组织维护的三件套已成为事实标准:


2.1 Unity:轻量级断言框架
Unity是一个专为嵌入式C设计的单元测试框架,仅由3个文件组成(unity.c、unity.h、unity_internals.h),零依赖、可移植性极强。它提供了丰富的断言宏:
| 断言宏 | 用途 |
|---|---|
TEST_ASSERT_EQUAL(expected, actual) |
整型相等断言 |
TEST_ASSERT_EQUAL_FLOAT(expected, actual, tolerance) |
浮点相等(带容差) |
TEST_ASSERT_NULL(pointer) |
空指针断言 |
TEST_ASSERT_BITS(mask, expected, actual) |
按位掩码断言 |
TEST_FAIL_MESSAGE(message) |
强制失败并输出消息 |
2.2 CMock:自动化桩函数生成器
CMock是Unity的"最佳拍档",它通过解析头文件自动生成Mock(桩函数)。开发者只需提供被调用模块的头文件,CMock即可生成:
- 调用计数追踪:验证函数被调用了几次
- 参数验证:检查传入的参数是否符合预期
- 返回值注入:控制Mock函数的返回值,模拟各种场景
- 回调函数:实现复杂的自定义桩逻辑
2.3 Ceedling:构建编排与测试管理
Ceedling是基于Ruby的构建系统,它将Unity和CMock无缝集成,提供:
- 自动测试发现:扫描
test_*.c文件并生成测试运行器 - 一键Mock生成:根据
project.yml配置自动调用CMock - 覆盖率报告:集成gcov/lcov生成HTML覆盖率报告
- 插件扩展:支持JSON报告、JUnit XML等CI/CD集成
三、测试驱动开发(TDD):红-绿-重构循环


TDD(Test-Driven Development)不是"先写测试再写代码"这么简单,它是一套严谨的工程纪律。对于嵌入式开发,TDD循环遵循红-绿-重构三步:
3.1 红色阶段:编写失败的测试
假设我们要实现一个温度传感器数据滤波器,要求对连续采样值做滑动平均。首先编写测试:
/* test_sensor_filter.c */
#include "unity.h"
#include "sensor_filter.h"
/* 每个测试前的初始化 */
void setUp(void)
{
SensorFilter_Init();
}
/* 每个测试后的清理 */
void tearDown(void)
{
/* 重置全局状态 */
}
/* 测试1:空缓冲区返回0 */
void test_SensorFilter_GetAverage_EmptyBuffer_ReturnsZero(void)
{
float result = SensorFilter_GetAverage();
TEST_ASSERT_EQUAL_FLOAT(0.0f, result, 0.001f);
}
/* 测试2:单点采样返回该值 */
void test_SensorFilter_GetAverage_SingleSample_ReturnsSameValue(void)
{
SensorFilter_AddSample(25.5f);
float result = SensorFilter_GetAverage();
TEST_ASSERT_EQUAL_FLOAT(25.5f, result, 0.001f);
}
/* 测试3:三点滑动平均 */
void test_SensorFilter_GetAverage_ThreeSamples_ReturnsAverage(void)
{
SensorFilter_AddSample(20.0f);
SensorFilter_AddSample(30.0f);
SensorFilter_AddSample(40.0f);
float result = SensorFilter_GetAverage();
TEST_ASSERT_EQUAL_FLOAT(30.0f, result, 0.001f);
}
/* 测试4:缓冲区溢出后覆盖最旧值 */
void test_SensorFilter_GetAverage_BufferOverflow_KeepsLastN(void)
{
/* 假设缓冲区大小为4 */
SensorFilter_AddSample(10.0f);
SensorFilter_AddSample(20.0f);
SensorFilter_AddSample(30.0f);
SensorFilter_AddSample(40.0f);
SensorFilter_AddSample(50.0f); /* 覆盖10.0f */
float result = SensorFilter_GetAverage();
TEST_ASSERT_EQUAL_FLOAT(35.0f, result, 0.001f); /* (20+30+40+50)/4 */
}
此时运行测试,所有测试都会失败(红色),因为sensor_filter.c尚未实现。
3.2 绿色阶段:编写最简实现
编写刚好能让测试通过的代码,不追求优雅:
/* sensor_filter.c */
#include "sensor_filter.h"
#define BUFFER_SIZE 4
static float buffer[BUFFER_SIZE];
static uint8_t count = 0;
static uint8_t index = 0;
void SensorFilter_Init(void)
{
count = 0;
index = 0;
memset(buffer, 0, sizeof(buffer));
}
void SensorFilter_AddSample(float value)
{
buffer[index] = value;
index = (index + 1) % BUFFER_SIZE;
if (count < BUFFER_SIZE) {
count++;
}
}
float SensorFilter_GetAverage(void)
{
if (count == 0) {
return 0.0f;
}
float sum = 0.0f;
for (uint8_t i = 0; i < count; i++) {
sum += buffer[i];
}
return sum / count;
}
运行测试,全部通过(绿色)。
3.3 重构阶段:优化代码结构
在测试保护下,安全地进行重构。例如将魔法数字改为宏、提取辅助函数、优化循环等。每次重构后运行测试,确保行为不变。
四、CMock桩函数:隔离硬件依赖的核心技术


嵌入式单元测试最大的挑战是硬件依赖。被测模块(SUT)往往直接调用HAL(硬件抽象层)函数,如HAL_ADC_GetValue()、HAL_PWM_SetDuty()等。这些函数操作寄存器,在PC上无法执行。
桩函数(Stub/Mock) 的核心思想是:用可控的替代函数替换真实的HAL调用,使测试可以在纯软件环境中运行。
4.1 CMock自动生成Mock
假设我们的电机控制器motor_controller.c依赖hal_pwm.h:
/* hal_pwm.h */
#ifndef HAL_PWM_H
#define HAL_PWM_H
#include <stdint.h>
/* 设置PWM占空比,返回0表示成功 */
int HAL_PWM_SetDuty(uint8_t channel, uint16_t duty);
/* 获取当前PWM频率 */
uint32_t HAL_PWM_GetFrequency(uint8_t channel);
/* 使能PWM输出 */
void HAL_PWM_Enable(uint8_t channel);
#endif
在project.yml中配置CMock:
# project.yml
:project:
:use_exceptions: FALSE
:use_test_preprocessor: TRUE
:use_auxiliary_dependencies: TRUE
:build_root: build
:paths:
:test:
- test/**
:source:
- src/**
:include:
- src/**
:cmock:
:mock_prefix: Mock
:when_no_prototypes: :warn
:enforce_strict_ordering: TRUE
:plugins:
- expect
- expect_any_args
- return_thru_ptr
- ignore
- callback
执行ceedling后,CMock自动生成Mockhal_pwm.c和Mockhal_pwm.h,包含以下函数:
/* Mockhal_pwm.h (CMock自动生成) */
void MockHAL_PWM_SetDuty_ExpectAndReturn(uint8_t channel, uint16_t duty, int cmock_to_return);
void MockHAL_PWM_SetDuty_ExpectAnyArgsAndReturn(int cmock_to_return);
void MockHAL_PWM_SetDuty_IgnoreAndReturn(int cmock_to_return);
void MockHAL_PWM_GetFrequency_ExpectAndReturn(uint8_t channel, uint32_t cmock_to_return);
void MockHAL_PWM_Enable_Expect(uint8_t channel);
void MockHAL_PWM_Enable_ExpectAnyArgs(void);
4.2 使用Mock编写测试
/* test_motor_controller.c */
#include "unity.h"
#include "motor_controller.h"
#include "Mockhal_pwm.h"
void setUp(void)
{
MotorController_Init();
}
void tearDown(void)
{
/* CMock自动验证所有期望 */
}
/* 测试:正常转速设置 */
void test_MotorController_SetSpeed_NormalValue_SetsPWMDuty(void)
{
/* 期望:调用HAL_PWM_SetDuty,通道0,占空比50%,返回0 */
MockHAL_PWM_SetDuty_ExpectAndReturn(0, 500, 0);
int result = MotorController_SetSpeed(0, 50); /* 50%转速 */
TEST_ASSERT_EQUAL(0, result);
}
/* 测试:HAL返回错误时,电机控制器应上报错误 */
void test_MotorController_SetSpeed_HALError_ReturnsError(void)
{
/* 模拟HAL返回错误 */
MockHAL_PWM_SetDuty_ExpectAndReturn(0, 500, -1);
int result = MotorController_SetSpeed(0, 50);
TEST_ASSERT_EQUAL(MOTOR_ERR_HW_FAIL, result);
}
/* 测试:使能电机时调用HAL使能 */
void test_MotorController_Enable_CallsHALEnable(void)
{
MockHAL_PWM_Enable_Expect(0);
MotorController_Enable(0);
}
/* 测试:多次设置速度,验证调用次数 */
void test_MotorController_SetSpeed_MultipleCalls_VerifyCount(void)
{
MockHAL_PWM_SetDuty_ExpectAndReturn(0, 300, 0);
MockHAL_PWM_SetDuty_ExpectAndReturn(0, 500, 0);
MockHAL_PWM_SetDuty_ExpectAndReturn(0, 800, 0);
MotorController_SetSpeed(0, 30);
MotorController_SetSpeed(0, 50);
MotorController_SetSpeed(0, 80);
}
4.3 高级Mock技巧:回调函数与复杂场景
对于需要动态行为的场景,CMock支持回调函数:
/* 定义回调函数,模拟温度传感器读取 */
static uint16_t mock_adc_values[] = {250, 260, 255, 270, 280};
static uint8_t mock_adc_index = 0;
uint16_t HAL_ADC_GetValue_Callback(uint8_t channel, int cmock_num_calls)
{
(void)channel;
if (mock_adc_index < sizeof(mock_adc_values)/sizeof(mock_adc_values[0])) {
return mock_adc_values[mock_adc_index++];
}
return 0;
}
void test_TemperatureMonitor_ReadSequence_AveragesCorrectly(void)
{
/* 注册回调,每次调用返回序列中的下一个值 */
HAL_ADC_GetValue_AddCallback(HAL_ADC_GetValue_Callback);
HAL_ADC_GetValue_ExpectAnyArgsAndReturn(0); /* 占位,实际由回调提供 */
HAL_ADC_GetValue_ExpectAnyArgsAndReturn(0);
HAL_ADC_GetValue_ExpectAnyArgsAndReturn(0);
HAL_ADC_GetValue_ExpectAnyArgsAndReturn(0);
HAL_ADC_GetValue_ExpectAnyArgsAndReturn(0);
mock_adc_index = 0;
float temp = TemperatureMonitor_ReadAverage(5); /* 读取5次取平均 */
/* (250+260+255+270+280)/5 = 263 */
TEST_ASSERT_EQUAL_FLOAT(263.0f, temp, 0.5f);
}
五、Ceedling工程实战:从零搭建测试项目
5.1 安装与环境配置
# 安装Ruby(Ceedling依赖)
sudo apt-get install ruby ruby-dev
# 安装Ceedling
gem install ceedling
# 验证安装
ceedling version
# Ceedling 0.31.1
5.2 项目目录结构
my_embedded_project/
├── project.yml # Ceedling主配置
├── src/ # 被测源代码
│ ├── sensor_filter.c
│ ├── sensor_filter.h
│ ├── motor_controller.c
│ ├── motor_controller.h
│ └── hal/
│ ├── hal_pwm.c
│ ├── hal_pwm.h
│ ├── hal_adc.c
│ └── hal_adc.h
├── test/ # 测试代码
│ ├── test_sensor_filter.c
│ ├── test_motor_controller.c
│ └── support/ # 测试辅助文件
│ └── test_runner.c
└── build/ # 构建输出(自动生成)
5.3 project.yml 完整配置
# project.yml
:project:
:use_exceptions: FALSE
:use_test_preprocessor: TRUE
:use_auxiliary_dependencies: TRUE
:build_root: build
:test_file_prefix: test_
:which_ceedling: gem
:default_tasks:
- test:all
:paths:
:test:
- +:test/**
- -:test/support
:source:
- src/**
:include:
- src/**
- src/hal/**
:support:
- test/support
:defines:
:common:
- UNIT_TEST
:test:
- TEST
:release:
- RELEASE
:cmock:
:mock_prefix: Mock
:when_no_prototypes: :warn
:enforce_strict_ordering: TRUE
:plugins:
- expect
- expect_any_args
- return_thru_ptr
- ignore
- callback
:treat_externs: :include
:strippables:
- '(?:__attribute__\s*\(+.*?\)+)'
:gcov:
:html_report: TRUE
:html_report_type: detailed
:reports:
- HtmlDetailed
- Text
- Cobertura
:utilities:
- gcov
- lcov
- gcovr
:plugins:
:load_paths:
- "#{Ceedling.load_path}"
:enabled:
- gcov
- junit_tests_report
- junit_tests_report
- warnings_report
:extension:
:header: .h
:source: .c
5.4 运行测试与查看报告
# 运行所有测试
ceedling test:all
# 运行特定模块测试
ceedling test:sensor_filter
# 生成覆盖率报告
ceedling gcov:all
# 查看详细覆盖率报告
# 打开 build/artifacts/gcov/GcovCoverageResults.html


六、硬件在环测试(HIL):从桌面到板级的跨越


单元测试在PC上验证了算法逻辑,但无法覆盖真实硬件时序、寄存器行为、电磁干扰等物理层面的问题。硬件在环测试(Hardware-in-the-Loop, HIL) 将真实硬件接入自动化测试框架,是嵌入式测试金字塔的顶层。
6.1 HIL测试系统组成
| 组件 | 功能 | 典型工具 |
|---|---|---|
| 测试主机 | 运行测试脚本,控制测试流程 | PC + Python/pytest |
| 通信接口 | PC与DUT的数据通道 | UART/USB转串口、J-Link、ST-Link |
| 被测设备(DUT) | 真实目标硬件 | 开发板/原型机 |
| 信号仿真 | 模拟传感器/负载信号 | 信号发生器、可编程电源、继电器阵列 |
| 数据采集 | 捕获DUT输出 | 示波器、逻辑分析仪 |
6.2 Python + pytest HIL测试框架
# test_hil_motor.py
import pytest
import serial
import time
import struct
class MotorHILFixture:
"""HIL测试夹具:管理串口通信和硬件状态"""
def __init__(self, port='/dev/ttyUSB0', baudrate=115200):
self.ser = serial.Serial(port, baudrate, timeout=2)
self.ser.reset_input_buffer()
self.ser.reset_output_buffer()
def send_command(self, cmd_id, payload=b''):
"""发送命令帧:帧头(2B) + 命令ID(1B) + 长度(1B) + 载荷 + 校验(1B)"""
frame = b'\\xAA\\x55' + bytes([cmd_id, len(payload)]) + payload
checksum = sum(frame) & 0xFF
frame += bytes([checksum])
self.ser.write(frame)
time.sleep(0.05) # 等待MCU处理
def read_response(self):
"""读取响应帧"""
# 等待帧头
header = self.ser.read(2)
if header != b'\\xAA\\x55':
raise TimeoutError("响应帧头错误")
cmd_id = self.ser.read(1)[0]
length = self.ser.read(1)[0]
payload = self.ser.read(length)
checksum = self.ser.read(1)[0]
# 校验验证
calc_checksum = sum(b'\\xAA\\x55' + bytes([cmd_id, length]) + payload) & 0xFF
assert checksum == calc_checksum, "校验失败"
return cmd_id, payload
def set_speed(self, channel, speed_percent):
"""设置电机转速"""
payload = struct.pack('<BH', channel, int(speed_percent * 10))
self.send_command(0x01, payload)
_, resp = self.read_response()
return resp[0] # 返回状态码
def get_speed(self, channel):
"""读取当前转速"""
payload = bytes([channel])
self.send_command(0x02, payload)
_, resp = self.read_response()
return struct.unpack('<H', resp)[0] / 10.0
def get_status(self):
"""读取系统状态"""
self.send_command(0x10, b'')
_, resp = self.read_response()
return {
'error_code': resp[0],
'temperature': struct.unpack('<h', resp[1:3])[0] / 10.0,
'voltage': struct.unpack('<H', resp[3:5])[0] / 100.0,
}
def reset(self):
"""硬件复位"""
# 通过DTR/RTS控制复位
self.ser.dtr = True
time.sleep(0.1)
self.ser.dtr = False
time.sleep(1.0) # 等待启动
def close(self):
self.ser.close()
@pytest.fixture
def motor_hil():
"""pytest夹具:每个测试用例前创建,测试后清理"""
hil = MotorHILFixture(port='/dev/ttyUSB0')
hil.reset()
yield hil
hil.close()
# ==================== 测试用例 ====================
class TestMotorBasic:
"""电机基础功能HIL测试"""
def test_set_speed_zero(self, motor_hil):
"""测试设置0%转速"""
status = motor_hil.set_speed(0, 0.0)
assert status == 0, "设置0%转速应成功"
actual = motor_hil.get_speed(0)
assert actual == pytest.approx(0.0, abs=0.1), f"实际转速应为0%,得到{actual}%"
n \n def test_set_speed_max(self, motor_hil):\n \"\"\"测试设置100%转速\"\"\"\n status = motor_hil.set_speed(0, 100.0)\n assert status == 0, \"设置100%转速应成功\"\n \n actual = motor_hil.get_speed(0)\n assert actual == pytest.approx(100.0, abs=0.5), f\"实际转速应为100%,得到{actual}%\"\n \n def test_set_speed_over_limit(self, motor_hil):\n \"\"\"测试超范围转速设置应被拒绝\"\"\"\n status = motor_hil.set_speed(0, 150.0)\n assert status == 0x02, \"超范围设置应返回参数错误\"\n\nclass TestMotorProtection:\n \"\"\"电机保护功能HIL测试\"\"\"\n \n def test_overtemperature_protection(self, motor_hil):\n \"\"\"测试过温保护:模拟高温环境\"\"\"\n # 先正常运行\n motor_hil.set_speed(0, 50.0)\n \n # 等待MCU温度采样(假设每100ms采样一次)\n time.sleep(0.5)\n \n status = motor_hil.get_status()\n # 如果板载温度超过阈值,应触发保护\n if status['temperature'] > 85.0:\n speed = motor_hil.get_speed(0)\n assert speed == 0.0, \"过温时应自动停机\"\n assert status['error_code'] == 0x10, \"应报过温错误\"\n \n def test_undervoltage_protection(self, motor_hil):\n \"\"\"测试欠压保护\"\"\"\n # 注意:此测试需要可编程电源配合\n # 此处假设通过命令模拟欠压状态\n motor_hil.set_speed(0, 50.0)\n \n status = motor_hil.get_status()\n if status['voltage'] < 10.0: # 欠压阈值10V\n speed = motor_hil.get_speed(0)\n assert speed == 0.0, \"欠压时应自动停机\"\n\nclass TestMotorPerformance:\n \"\"\"电机性能HIL测试\"\"\"\n \n def test_speed_ramp_response(self, motor_hil):\n \"\"\"测试转速斜坡响应时间\"\"\"\n motor_hil.set_speed(0, 0.0)\n time.sleep(0.1)\n \n start_time = time.time()\n motor_hil.set_speed(0, 80.0)\n \n # 轮询直到达到目标转速的95%\n timeout = 2.0\n while time.time() - start_time < timeout:\n actual = motor_hil.get_speed(0)\n if actual >= 76.0: # 80% * 95%\n response_time = time.time() - start_time\n assert response_time < 1.0, f\"响应时间{response_time}s超过1s限制\"\n return\n time.sleep(0.01)\n \n pytest.fail(\"未能在超时内达到目标转速\")\n \n def test_speed_stability(self, motor_hil):\n \"\"\"测试稳态转速波动\"\"\"\n motor_hil.set_speed(0, 50.0)\n time.sleep(1.0) # 等待稳定\n \n samples = []\n for _ in range(100):\n samples.append(motor_hil.get_speed(0))\n time.sleep(0.01)\n \n avg = sum(samples) / len(samples)\n variance = sum((x - avg) ** 2 for x in samples) / len(samples)\n std_dev = variance ** 0.5\n \n assert std_dev < 0.5, f\"转速标准差{std_dev}超过0.5%限制\"\n\n# 运行测试:pytest test_hil_motor.py -v --tb=short
6.3 HIL测试的自动化信号仿真
对于需要外部信号输入的测试,可以结合继电器阵列和DAC构建自动化信号仿真平台:
# signal_simulator.py
import smbus2
import RPi.GPIO as GPIO
import time
class SignalSimulator:
\"\"\"基于树莓派的信号仿真器\"\"\"
n \n def __init__(self):\n self.i2c = smbus2.SMBus(1)\n self.dac_addr = 0x48 # MCP4725 DAC地址\n GPIO.setmode(GPIO.BCM)\n # 配置8路继电器\n self.relay_pins = [17, 27, 22, 23, 24, 25, 5, 6]\n for pin in self.relay_pins:\n GPIO.setup(pin, GPIO.OUT)\n GPIO.output(pin, GPIO.LOW)\n \n def set_analog_voltage(self, voltage):\n \"\"\"设置模拟电压输出(0-3.3V)\"\"\"\n value = int((voltage / 3.3) * 4095)\n self.i2c.write_word_data(self.dac_addr, 0x40, (value & 0xFF) << 4 | (value >> 8))\n \n def set_relay(self, channel, state):\n \"\"\"设置继电器状态\"\"\"\n GPIO.output(self.relay_pins[channel], GPIO.HIGH if state else GPIO.LOW)\n \n def simulate_sensor_fault(self, sensor_channel):\n \"\"\"模拟传感器断路故障\"\"\"\n self.set_relay(sensor_channel, False)\n \n def simulate_sensor_short(self, sensor_channel):\n \"\"\"模拟传感器短路到地\"\"\"\n self.set_relay(sensor_channel + 4, True) # 使用另一组继电器接地\n \n def cleanup(self):\n GPIO.cleanup()
七、测试金字塔:三层测试策略
高效的嵌入式测试体系应遵循测试金字塔原则:
| 层级 | 比例 | 工具 | 执行环境 | 目标 |
|---|---|---|---|---|
| 单元测试 | 70% | Unity + CMock + Ceedling | PC主机(x86模拟) | 验证函数逻辑、边界条件、异常处理 |
| 集成测试 | 20% | Ceedling + 自定义桩 | PC主机或仿真器 | 验证模块间接口、状态机转换 |
| HIL测试 | 10% | Python + pytest + 真实硬件 | 目标硬件 | 验证真实时序、硬件交互、长期稳定性 |
关键原则:
- 单元测试要"快":单个测试应在毫秒级完成,全套件在秒级完成
- Mock要"真":桩函数行为应精确模拟真实硬件的边界条件
- HIL要"精":聚焦单元测试无法覆盖的硬件相关场景,避免重复验证
八、CI/CD 集成:自动化测试流水线

将测试集成到CI/CD流水线,实现每次代码提交自动验证:
8.1 GitHub Actions 配置
# .github/workflows/embedded-test.yml
name: Embedded CI Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
unit-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.1'
- name: Install Ceedling
- run: gem install ceedling
- name: Install GCC
run: sudo apt-get update && sudo apt-get install -y gcc gcovr lcov
- name: Run Unit Tests
run: ceedling test:all
- name: Generate Coverage Report
run: ceedling gcov:all
- name: Upload Coverage
uses: codecov/codecov-action@v3
with:
files: build/artifacts/gcov/GcovCoverageResults.xml
fail_ci_if_error: true
static-analysis:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Cppcheck
run: |
sudo apt-get install -y cppcheck
cppcheck --enable=all --error-exitcode=1 \\
--suppress=missingIncludeSystem \\
src/
- name: Run MISRA-C Check
run: |
# 使用PC-lint或Cppcheck MISRA插件
cppcheck --addon=misra.json src/
hil-test:
runs-on: self-hosted # 使用连接了HIL硬件的自托管Runner
needs: [unit-test, static-analysis]
steps:
- uses: actions/checkout@v4
- name: Build Firmware
run: |
make clean
make CFLAGS=\"-DDEBUG -O0\"
- name: Flash Firmware
run: |
st-flash write build/firmware.bin 0x8000000
- name: Run HIL Tests
run: |
pip install pytest pyserial
pytest test/hil/ -v --tb=short --junitxml=report.xml
- name: Upload HIL Report
uses: actions/upload-artifact@v4
with:
name: hil-test-report
path: report.xml
8.2 覆盖率门禁策略
# 在CI中设置覆盖率阈值
- name: Check Coverage Threshold
run: |
COVERAGE=$(grep -oP 'lines-covered=\"\\K[0-9]+' build/artifacts/gcov/coverage.xml)
TOTAL=$(grep -oP 'lines-valid=\"\\K[0-9]+' build/artifacts/gcov/coverage.xml)
RATE=$(echo "scale=2; $COVERAGE / $TOTAL * 100" | bc)
echo \"代码覆盖率: ${RATE}%\"\n if (( $(echo \"$RATE < 80\" | bc -l) )); then\n echo \"错误:覆盖率低于80%阈值\"\n exit 1\n fi
九、常见问题与最佳实践
9.1 如何处理静态变量和全局状态?
嵌入式代码常使用静态变量保存状态,这会导致测试间相互影响。解决方案:
/* sensor_filter.h */
#ifdef UNIT_TEST
/* 测试模式下暴露内部状态 */
extern uint8_t g_bufferCount;
extern float g_buffer[];
void SensorFilter_ResetInternalState(void);
#endif
/* sensor_filter.c */
#ifdef UNIT_TEST
uint8_t g_bufferCount = 0;
float g_buffer[BUFFER_SIZE] = {0};
void SensorFilter_ResetInternalState(void) {
g_bufferCount = 0;
memset(g_buffer, 0, sizeof(g_buffer));
}
#else
static uint8_t g_bufferCount = 0;
static float g_buffer[BUFFER_SIZE] = {0};
#endif
在setUp()中调用SensorFilter_ResetInternalState()确保测试隔离。
9.2 如何处理中断和时序?
中断和严格时序是单元测试的难点。策略:
- 中断解耦:将中断服务程序(ISR)中的逻辑提取为普通函数,ISR仅做最小调度
- 模拟时间:使用可注入的时钟源替代
HAL_GetTick() - HIL覆盖:时序相关测试留给HIL阶段
/* 可注入的时钟接口 */
uint32_t (*g_getTickFunc)(void) = HAL_GetTick;
uint32_t GetCurrentTick(void) {
return g_getTickFunc();
}
#ifdef UNIT_TEST
void SetMockTickFunc(uint32_t (*func)(void)) {
g_getTickFunc = func;
}
#endif
9.3 如何处理内存受限的断言?
嵌入式系统内存有限,避免使用大型缓冲区做字符串比较:
/* 不推荐:可能栈溢出 */
TEST_ASSERT_EQUAL_STRING(long_string, result);
/* 推荐:使用长度限制的比较 */
TEST_ASSERT_EQUAL_MEMORY(expected, result, sizeof(expected));
TEST_ASSERT_EQUAL_STRING_LEN(expected, result, MAX_LEN);
9.4 测试命名规范
清晰的测试命名是测试可维护性的关键:
/* 格式:test_<被测函数>_<场景/条件>_<预期结果> */
void test_SensorFilter_AddSample_NullPointer_ReturnsError(void);
void test_MotorController_SetSpeed_NegativeValue_IgnoresInput(void);
void test_CommProtocol_ParseFrame_ValidCRC_ReturnsSuccess(void);
void test_CommProtocol_ParseFrame_InvalidCRC_ReturnsChecksumError(void);
void test_BatteryMonitor_GetLevel_LowVoltage_ReturnsCritical(void);
十、总结与展望
本文系统性地介绍了嵌入式C语言单元测试的完整技术栈:
| 技术点 | 核心要点 |
|---|---|
| Unity | 轻量级断言框架,3文件零依赖 |
| CMock | 头文件解析自动生成Mock,隔离硬件依赖 |
| Ceedling | 一键构建、测试发现、覆盖率报告 |
| TDD | 红-绿-重构循环,先写测试再写实现 |
| HIL | Python/pytest驱动真实硬件,覆盖物理层场景 |
| CI/CD | GitHub Actions自动化流水线,覆盖率门禁 |
在鸿蒙生态(OpenHarmony/HarmonyOS)的嵌入式开发中,这些技术同样适用。无论是轻量级的轻内核(LiteOS-M)还是标准内核(LiteOS-A),Unity/CMock的零依赖特性使其可以轻松集成到鸿蒙的编译系统中。结合鸿蒙的HDF(Hardware Driver Foundation)框架,可以通过Mock HDF接口实现驱动层的单元测试,进一步提升鸿蒙设备驱动的质量与可靠性。
测试不是开发的负担,而是质量的护城河。 当每一次git push都能在25分钟内完成从静态分析到HIL验证的全流程自动化测试时,团队才能真正实现"快速迭代、 confidently 交付"。
转载自:https://blog.csdn.net/u014727709/article/details/162584162
欢迎 👍点赞✍评论⭐收藏,欢迎指正
更多推荐


所有评论(0)