在这里插入图片描述

每日一句正能量

筛选比改变更重要。
成年人的关系和时间都很珍贵。试图改变别人往往徒劳且消耗自己,而懂得筛选对的人、对的事,则是一种更高效、更清醒的生活策略。

一、前言:为什么嵌入式软件必须做单元测试?

在嵌入式开发领域,"代码能跑就行"的时代早已过去。随着物联网、智能汽车、工业控制等场景的复杂度指数级增长,嵌入式固件的可靠性和可维护性已成为产品成败的关键。据统计,缺陷修复成本在编码阶段仅为1倍,到系统集成阶段飙升至10倍,而部署到现场后可达100倍以上。对于运行在资源受限MCU上的固件,一旦出现问题,OTA升级困难、现场调试成本高,甚至涉及人身安全。

传统的嵌入式测试往往依赖"printf调试"和"板上验证",这种方式存在三大痛点:

  1. 硬件依赖性强:没有开发板就无法测试,团队成员需要排队等待硬件资源
  2. 反馈周期长:修改一行代码→编译→烧录→运行→观察,循环动辄数分钟
  3. 边界条件难覆盖:极端温度、传感器故障、通信超时等场景难以在真实环境中复现

单元测试(Unit Testing) 正是解决这些痛点的银弹。通过在PC主机上模拟目标环境,开发者可以在秒级获得测试反馈,自动化覆盖正常路径与异常分支。本文将深入讲解嵌入式C语言单元测试的黄金组合——Unity + CMock + Ceedling,并结合**硬件在环测试(HIL)**构建从桌面到板级的完整测试体系。


二、测试工具链全景:Unity / CMock / Ceedling

嵌入式C语言单元测试领域,ThrowTheSwitch组织维护的三件套已成为事实标准:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

2.1 Unity:轻量级断言框架

Unity是一个专为嵌入式C设计的单元测试框架,仅由3个文件组成(unity.cunity.hunity_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.cMockhal_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 如何处理中断和时序?

中断和严格时序是单元测试的难点。策略

  1. 中断解耦:将中断服务程序(ISR)中的逻辑提取为普通函数,ISR仅做最小调度
  2. 模拟时间:使用可注入的时钟源替代HAL_GetTick()
  3. 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
欢迎 👍点赞✍评论⭐收藏,欢迎指正

更多推荐