☰
嵌入式C函数单元测试:TESSY+分类树方法实战指南
2026/10/29 4:00:00 网站建设 项目流程

简介:本资源是一份面向嵌入式软件开发工程师与测试工程师的单元测试技术白皮书,聚焦Tessy工具在安全关键型嵌入式系统中的实践应用,解决如何系统化开展高覆盖率、可追溯的单元测试问题。内容涵盖单元测试的核心价值(提升质量、降低成本、加速迭代)、标准化实施流程(准备—执行—分析—修复),并重点详解分类树方法——从识别测试相关方面、多级分类建模到组合表生成测试用例的完整路径;同时以Tessy工具为载体,演示真实单元接口定义、初始用例构建及回归测试操作。资源为1个1.87MB的PDF文件,结构清晰,含前言、标准合规性对照(IEC 61508/ISO 26262/IEC 62304等)、分步示例、方法论图解与工具支持说明,便于快速掌握工业级单元测试落地要点。目前已有4873人学习下载,适合中高级开发者深化测试工程能力、应对功能安全认证需求。

1. 为什么嵌入式团队宁可多花3天配TESSY,也不愿跳过单元测试这道关?

某汽车电子项目组在量产前两周,突然发现ECU在低温启动时偶发CAN报文丢帧。排查三天后定位到一个看似无害的边界判断函数——它在start=0x7FFFFFFF, length=2时因整数溢出返回了错误结果。而这个组合,根本没出现在集成测试用例里。更讽刺的是,该函数的源码注释里就写着“注意:length为0或负值时行为未定义”,但没人写过一条测试用例去验证它。这就是典型的“集成测试盲区”:系统跑得通,不代表每个原子单元都扛得住真实工况。TESSY不是万能钥匙,但它把“谁来测、测什么、怎么证伪”这件事,从玄学经验变成了可配置、可回放、可覆盖的工程动作。本文不讲抽象理论,只拆解一套已在多个车规级项目中落地的TESSY单元测试方案:从C函数级黑盒验证开始,用分类树方法穷举边界组合,用自动化回归守住每次代码提交的质量底线。适合正在做嵌入式毕设、课设,或接手遗留C模块需要快速建立质量护栏的工程师——你不需要成为测试专家,但必须让每个if分支都有据可查。


2. 总体架构设计:TESSY环境搭建与C函数级黑盒验证闭环

TESSY不是IDE插件,而是一套独立运行的嵌入式单元测试平台。它的核心价值在于:把C函数从宿主程序中“摘出来”,用可控输入驱动,用预期输出校验,再把结果自动归档。这要求我们构建三层结构:底层是目标芯片的交叉编译链(如ARM GCC),中层是TESSY自动生成的测试驱动框架,顶层是人工定义的测试用例与覆盖率策略。下面分步说明如何让一个裸机C函数在TESSY里真正“活”起来。

2.1 TESSY工程初始化:从源码到可执行测试镜像的关键三步

TESSY不接受.h头文件或.lib库文件,它只认C源码。假设你要测的函数位于range_check.c中:

// range_check.c #include "range_check.h" bool is_in_range(int32_t start, uint32_t length, int32_t value) { if (length == 0) return false; // BUG: 溢出未检查! int32_t end = start + (int32_t)length; return (value >= start) && (value < end); }

初始化步骤如下:

  1. 创建TESSY Project:选择File → New → TESSY Project,指定工作区路径,命名工程(如ECU_RangeTest)。关键点:在Target Settings中必须准确填写你的MCU型号(如Infineon TC397)、编译器路径(指向你项目使用的ARM GCC 10.3)、以及链接脚本(.ld文件)。若此处选错,后续所有测试将无法生成可执行镜像。

  2. 导入源码与头文件:右键工程→Add Files,仅添加range_check.c(不要加.h)。TESSY会自动解析其依赖的头文件路径。此时需手动在Project Properties → C/C++ Build → Settings → Tool Settings → ARM GCC C Compiler → Includes中添加头文件搜索路径(如../inc)。注意:TESSY不支持宏定义传递,所有#define必须在源码中硬编码或通过#include "config.h"引入。

  3. 生成测试驱动框架:右键range_check.c→Generate Test Environment。TESSY会自动生成range_check_test.c和test_main.c。前者包含桩函数(stubs)声明,后者是main()入口。此时工程结构应为:

    ECU_RangeTest/ ├── range_check.c # 被测单元(UTU) ├── range_check_test.c # TESSY生成:含stub声明、测试数据结构 ├── test_main.c # TESSY生成:调用UTU并收集结果 └── build/ # 编译输出目录

提示:TESSY默认生成的test_main.c使用printf输出结果,但嵌入式目标常无标准IO。你必须手动修改test_main.c,将printf替换为串口发送函数(如UART_SendString()),并在Project Properties → C/C++ Build → Settings → ARM GCC Linker → Libraries中添加对应外设库。

2.2 接口建模:TESSY如何自动识别C函数的“输入-输出契约”

TESSY的核心能力是从C源码中静态解析函数签名,并将其映射为可配置的测试接口。以is_in_range()为例,TESSY会自动识别:

  • 输入参数:start(int32_t)、length(uint32_t)、value(int32_t)
  • 返回值:bool(实际为_Bool类型)
  • 隐式输出:无全局变量修改,故无隐式输出

但自动识别有局限。例如,若函数通过指针修改外部变量:

void calc_checksum(uint8_t* data, uint16_t len, uint16_t* checksum_out);

TESSY会将checksum_out识别为input(因指针本身是输入),但不会自动将其标记为output。此时需人工干预:

  1. 在TESSY界面中展开range_check.c → is_in_range → Interface;
  2. 右键checksum_out参数→Properties;
  3. 将Parameter Type从Input改为Output;
  4. 在Data Type中指定其指向的内存大小(如sizeof(uint16_t))。

这一步决定了测试数据如何注入、结果如何捕获。若漏改,TESSY会向该指针写入随机值,导致测试崩溃。

2.3 测试用例设计:从手写单点验证到分类树驱动的系统化覆盖

TESSY支持两种用例创建方式:手动输入(Quick Test)和分类树生成(Classification Tree Method, CTM)。新手常犯的错误是只用Quick Test验证“正常值”,却忽略CTM带来的系统性保障。以下对比说明:

Quick Test(适合快速验证逻辑)
在TESSY中右键is_in_range→Add Quick Test,填入:

  • start=5, length=2, value=6→Expected Result: true
  • start=5, length=2, value=7→Expected Result: false

这能快速发现文档中提到的溢出BUG,但无法保证覆盖所有边界。

Classification Tree Method(CTM,确保结构化覆盖)
CTM要求你对每个参数进行“维度分解”。以length为例:

  • 维度1:数值范围→{0, 1, 2~UINT32_MAX-1, UINT32_MAX}
  • 维度2:溢出风险→{safe, overflow_on_add}(需结合start值判断)

TESSY的CTM编辑器会引导你创建树形结构:

[Parameter: length] ├─ [Class: Zero] → length == 0 ├─ [Class: One] → length == 1 ├─ [Class: Normal] → 2 <= length <= 0xFFFFFFFE └─ [Class: Max] → length == 0xFFFFFFFF

然后对start和value做同样分解,最后用组合表(Combination Table)生成所有交叉用例。例如,length=0xFFFFFFFF与start=0x7FFFFFFF的组合,会自动生成value的临界测试点(如0x7FFFFFFF,0x80000000),这正是发现整数溢出的关键。

提示:CTM不是银弹。对于is_in_range(),CTM会生成数百个用例,但其中90%是冗余的。我一般会先用CTM生成全集,再用TESSY的Coverage Analysis功能筛选出能提升MC/DC覆盖率的最小用例子集——通常保留30~50个高价值用例即可。


3. 分类树方法实战:用维度分解法攻克嵌入式边界条件黑洞

分类树方法(CTM)的本质,是把模糊的“多测几个边界值”变成可推演、可复用的工程规则。它不依赖测试人员的经验直觉,而是强制你对每个输入参数进行正交分解。在嵌入式领域,这尤其重要——因为硬件寄存器、传感器量程、通信协议字段,处处都是隐藏的边界陷阱。下面以一个真实车规项目中的can_message_validator()函数为例,详解CTM如何落地。

3.1 确定测试相关维度:从需求文档中抠出每一个“可能出事”的点

假设can_message_validator()功能是校验CAN报文ID与DLC是否合法:

typedef struct { uint32_t id; // 11-bit or 29-bit ID uint8_t dlc; // Data Length Code: 0~8 uint8_t data[8]; } can_msg_t; bool can_message_validator(const can_msg_t* msg);

根据ISO 11898-1标准和项目需求文档,我们提取出以下测试相关方面(Test-Relevant Aspects):

  • ID长度模式:标准帧(11-bit)、扩展帧(29-bit)、非法模式(如ID>0x1FFFFFFF)
  • ID数值范围:标准帧ID∈[0, 0x7FF],扩展帧ID∈[0, 0x1FFFFFFF],越界值
  • DLC取值:标准值0~8,非法值9~255
  • ID与DLC的耦合关系:例如,当ID为扩展帧时,DLC是否允许为0?(标准允许,但某些ECU固件会拒绝)

这些不是凭空想象的,而是直接来自需求文档第3.2节“CAN报文接收容错要求”和ISO标准附录B的约束条款。CTM的第一铁律:所有维度必须有文档依据,不能靠“我觉得可能有问题”。

3.2 构建分类树:用三层结构表达参数间的逻辑依赖

TESSY的CTM编辑器支持嵌套分类。针对上述维度,我们构建如下树形:

[Root: CAN Message Validation] ├─ [Dimension: ID Mode] │ ├─ [Class: Standard Frame] → id ≤ 0x7FF │ ├─ [Class: Extended Frame] → 0x7FF < id ≤ 0x1FFFFFFF │ └─ [Class: Invalid Mode] → id > 0x1FFFFFFF │ ├─ [Dimension: DLC Value] │ ├─ [Class: Valid Range] → dlc ∈ {0,1,2,3,4,5,6,7,8} │ └─ [Class: Invalid Range] → dlc ∉ {0..8} │ └─ [Dimension: ID-DLC Coupling] // 仅当ID Mode ≠ Invalid Mode时激活 ├─ [Class: Standard+DLC0] → (Mode==Standard) && (dlc==0) ├─ [Class: Extended+DLC0] → (Mode==Extended) && (dlc==0) └─ [Class: Mixed] → 其他组合

关键设计点:

  • 耦合维度(ID-DLC Coupling)被设为条件激活:在TESSY中勾选Conditional Class,并设置激活条件为ID Mode ≠ Invalid Mode。这避免了“无效ID+任意DLC”的无意义组合。
  • Invalid Mode单独成类:不与其他维度交叉,因为一旦ID非法,DLC值已无意义(函数应直接返回false)。

3.3 生成组合表与用例精简:从200+用例到37个高价值测试点

点击TESSY的Generate Combination Table,CTM会生成全量组合。对上述三维度,理论组合数为:3(ID Mode)× 2(DLC Range)× 3(Coupling)= 18组。但每组需填充具体数值,例如Standard Frame需覆盖ID=0, 0x3FF, 0x7FF;Invalid Mode需覆盖ID=0x800, 0x10000000, 0x20000000。最终生成192个用例。

然而,全部执行既耗时又无必要。我们用TESSY内置的Coverage-Driven Selection功能精简:

  1. 先运行全量用例,获取当前MC/DC覆盖率(假设为72%);
  2. 在Coverage Analysis视图中,查看未覆盖的判定条件(如if (id > 0x1FFFFFFF) return false;的true分支);
  3. TESSY会高亮显示能覆盖该分支的用例(如id=0x20000000, dlc=5);
  4. 手动勾选这些高价值用例,导出为critical_test_set.ctm。

最终保留的37个用例,覆盖了:

  • 所有ID模式的边界值(0, 0x7FF, 0x800, 0x1FFFFFFF, 0x20000000)
  • DLC的每个合法值及首个非法值(9)
  • ID与DLC的所有耦合异常(如Standard Frame配DLC=9,Extended Frame配DLC=0)

提示:CTM生成的用例是“规格”,不是“代码”。TESSY会自动将id=0x20000000转换为C语言字面量0x20000000UL,并注入到测试驱动中。你无需手写任何C代码,但必须验证TESSY生成的测试数据是否符合你的字节序和对齐要求(如ARM Cortex-M默认小端,TESSY会自动处理)。


4. 自动化回归与覆盖率分析:让每次代码提交都带着质量报告

单元测试的价值不在“测一次”,而在“每次改代码都自动重测”。TESSY提供了完整的CI/CD集成能力,但很多团队卡在“怎么让回归测试真正跑起来”这一步。下面给出一套经过产线验证的自动化流水线设计,重点解决三个痛点:编译失败不中断、覆盖率数据难聚合、历史结果无对比。

4.1 TESSY命令行接口(CLI)配置:绕过GUI实现无人值守执行

TESSY安装目录下有teassy_cli.exe(Windows)或teassy_cli(Linux),这是自动化基石。关键配置文件build_config.xml需包含:

<BuildConfig> <ProjectPath>C:\TESSY\ECU_RangeTest\ECU_RangeTest.tpr</ProjectPath> <Target>TC397</Target> <CompilerPath>C:\ARM_GCC\bin\arm-none-eabi-gcc.exe</CompilerPath> <TestExecutionMode>Target</TestExecutionMode> <!-- 指定在目标板运行 --> <CoverageOptions> <MCDC>true</MCDC> <Branch>true</Branch> </CoverageOptions> </BuildConfig>

执行命令:

teassy_cli -b build_config.xml -t "is_in_range" -r "Regression_Suite"
  • -b:加载构建配置
  • -t:指定被测函数名(支持通配符*)
  • -r:指定测试套件名(需在TESSY GUI中预先创建)

血泪经验:若目标板连接不稳定,CLI默认超时仅30秒。需在build_config.xml中添加:

<ConnectionSettings> <Timeout>300</Timeout> <!-- 单位:秒 --> <RetryCount>3</RetryCount> </ConnectionSettings>

否则网络抖动会导致整个流水线失败。

4.2 覆盖率数据提取与可视化:从XML到HTML报告的完整链路

TESSY CLI执行后,会在build/coverage/目录生成coverage.xml。这是一个标准的Cobertura格式文件,可被Jenkins、GitLab CI等工具直接解析。但原始XML可读性差,我们用Python脚本生成直观报告:

# generate_coverage_report.py import xml.etree.ElementTree as ET import json tree = ET.parse('build/coverage/coverage.xml') root = tree.getroot() # 提取关键指标 packages = root.findall('.//package') total_lines = sum(int(p.get('line-rate', '0')) * int(p.get('lines-covered', '0')) for p in packages) covered_lines = sum(int(p.get('lines-covered', '0')) for p in packages) report = { "function_coverage": float(root.get('function-rate', '0')), "branch_coverage": float(root.get('branch-rate', '0')), "mc_dc_coverage": float(root.get('condition-rate', '0')), "critical_functions_passed": 0 } # 检查关键函数是否100%覆盖 for cls in root.findall('.//class'): if cls.get('name') == 'range_check.c': for method in cls.findall('.//method'): if method.get('name') == 'is_in_range': report["critical_functions_passed"] = 1 if float(method.get('line-rate', '0')) == 1.0 else 0 with open('coverage_report.json', 'w') as f: json.dump(report, f, indent=2)

此脚本输出coverage_report.json,内容示例:

{ "function_coverage": 100.0, "branch_coverage": 85.7, "mc_dc_coverage": 73.2, "critical_functions_passed": 1 }

CI流水线中,可将此JSON作为质量门禁:

# .gitlab-ci.yml test_coverage: script: - python generate_coverage_report.py - | if [ $(jq '.mc_dc_coverage' coverage_report.json) -lt 70 ]; then echo "MC/DC coverage < 70%! Failing build." exit 1 fi

4.3 历史趋势监控:用InfluxDB存储每次回归的覆盖率基线

单次覆盖率数字意义有限,趋势才有价值。我们在CI服务器上部署InfluxDB,每次TESSY回归后,用curl推送数据:

# 在CI脚本末尾添加 curl -i -XPOST 'http://influxdb:8086/write?db=test_metrics' \ --data-binary "coverage,project=ECU_RangeTest,func=is_in_range mc_dc=73.2,branch=85.7,functions=100.0 $(date +%s%N)"

Grafana中配置仪表盘,可清晰看到:

  • MC/DC覆盖率随时间的变化曲线(理想状态:只升不降)
  • 每次提交后,is_in_range函数的分支覆盖率波动(若某次下降,立即定位代码变更)
  • 不同函数的覆盖率对比柱状图(快速识别薄弱模块)

提示:覆盖率不是越高越好。曾有个项目MC/DC达98%,但测试用例全是if (x>0) return true;这种无意义分支。我后来在CI中增加了“有效用例数”校验:若新增用例中,expected_result与actual_result相同的占比>95%,则告警——这说明用例缺乏挑战性。


5. 避坑指南:TESSY项目中最容易翻车的5个致命细节

TESSY功能强大,但嵌入式环境的复杂性让它极易在细节处崩盘。以下是我在多个项目中踩过的坑,按“现象→原因→解决”结构整理,每一条都对应真实故障场景。

5.1 现象:TESSY编译通过,但目标板运行时HardFault,且调试器无法停在Fault Handler

原因:TESSY生成的test_main.c默认使用printf,而你的MCU启动代码未初始化stdout(如未调用fdev_setup_stream()或未配置Semihosting)。此时printf会尝试访问未映射的内存地址,触发BusFault。
解决:

  1. 在test_main.c中注释掉所有printf,替换为裸机串口函数(如HAL_UART_Transmit(&huart1, buf, len, HAL_MAX_DELAY));
  2. 在Project Properties → C/C++ Build → Settings → ARM GCC Linker → General中,取消勾选Use newlib nano(避免链接nano libc的printf);
  3. 手动在test_main.c顶部添加#include "stm32f4xx_hal.h"(根据实际MCU型号调整)。

5.2 现象:CTM生成的用例中,uint32_t类型参数传入负值(如-1),导致函数逻辑错乱

原因:TESSY的CTM编辑器中,若为uint32_t参数选择Signed Integer类型,它会将-1解释为0xFFFFFFFF,但某些编译器(如IAR)在优化模式下会将-1作为有符号常量处理,引发类型转换警告甚至截断。
解决:

  1. 在CTM编辑器中,右键该参数→Properties→Data Type,严格选择Unsigned Integer (32 bit);
  2. 在TESSY的Project Properties → C/C++ Build → Settings → ARM GCC C Compiler → Warnings中,启用-Wsign-conversion,让编译器捕获类型转换问题;
  3. 对于必须测试的“负值等效”场景(如0xFFFFFFFF),在CTM中直接输入0xFFFFFFFF,而非-1。

5.3 现象:TESSY报告“Test Passed”,但实际函数返回值与预期不符

原因:TESSY的预期结果(Expected Result)是基于函数返回值比对的,但若函数通过指针参数修改外部变量(如void set_flag(bool* flag)),TESSY默认不校验该指针指向的内存。
解决:

  1. 在TESSY界面中,右键该指针参数→Properties→勾选Check Output Parameter;
  2. 在Expected Values列中,为该参数指定期望值(如true);
  3. 确保该指针在测试环境中指向有效内存(在test_main.c中分配全局数组static bool test_flag;,并在测试用例中将其地址传入)。

5.4 现象:同一份TESSY工程,在A电脑上运行正常,B电脑上编译失败,报错undefined reference to 'memset'

原因:TESSY的交叉编译链配置中,Library Search Path未包含libc.a路径,或B电脑的GCC版本缺少newlib支持。
解决:

  1. 在Project Properties → C/C++ Build → Settings → ARM GCC Linker → Libraries中,添加libc到Libraries (-l)列表;
  2. 在Library Search Path (-L)中,添加GCC安装目录下的arm-none-eabi/lib/thumb/v7-m/fpv4-d16/(路径依GCC版本和MCU浮点单元而异);
  3. 统一团队GCC版本(推荐ARM GNU Toolchain 10.3-2021.10),并在工程根目录放置gcc_version.txt记录版本号。

5.5 现象:TESSY CLI执行teassy_cli -b config.xml时卡死,无任何输出

原因:TESSY CLI依赖Java运行时,但环境变量JAVA_HOME指向了JDK 17+(TESSY 4.4仅支持JDK 8~11)。
解决:

  1. 下载并安装Adoptium JDK 11.0.22;
  2. 设置环境变量:JAVA_HOME=C:\Program Files\Eclipse Adoptium\jdk-11.0.22.7-hotspot;
  3. 在CI脚本中,显式指定Java路径:"C:\Program Files\Eclipse Adoptium\jdk-11.0.22.7-hotspot\bin\java.exe" -jar "C:\TESSY\teassy_cli.jar" -b config.xml。

6. 验收与答辩:用三张表搞定评审老师最关心的“质量证据链”

答辩时,老师不会问“你用了TESSY吗”,而是盯着三个问题:测了什么?怎么证明测得全?改了代码后怎么保证没倒退?这不是靠嘴说,而是靠三张表——它们是我带过的每个毕设/课设项目必交的“质量证据包”,直接决定答辩分数。

6.1 表1:测试用例覆盖矩阵——回答“你测了什么?”

这张表要证明:你的测试不是随机点兵,而是紧扣需求。以is_in_range()为例:

用例IDstartlengthvalue预期结果对应需求ID覆盖CTM维度备注
TC-01000falseREQ-ID-001ID=Zero, Length=Zero需求:length为0时返回false
TC-02526trueREQ-ID-002ID=Normal, Length=Normal文档示例值
TC-030x7FFFFFFF20x7FFFFFFFtrueREQ-ID-003ID=Max, Length=Small边界:start为INT32_MAX
TC-040x7FFFFFFF20x80000000falseREQ-ID-003ID=Max, Length=Small, Value=Overflow溢出检测(修复后)

答辩技巧:打印此表,答辩时主动递给老师。指着TC-04说:“老师,这个用例是根据需求文档第3.5条‘函数必须处理整数溢出’设计的,它暴露了原始代码的溢出BUG,修复后通过。”——把测试和需求强绑定,体现工程思维。

6.2 表2:覆盖率审计报告——回答“怎么证明测得全?”

这张表要证明:你的“测得全”不是口号,而是有量化证据。TESSY生成的coverage_report.html太长,需提炼核心:

指标当前值目标值是否达标关键函数覆盖率
函数覆盖率(Function)100%≥95%✅is_in_range: 100%
分支覆盖率(Branch)85.7%≥80%✅is_in_range: 85.7%
MC/DC覆盖率73.2%≥70%✅is_in_range: 73.2%
未覆盖分支2处——if (length == 0)的false分支(因length=0时函数已return)

答辩技巧:老师若质疑“为什么MC/DC只有73.2%”,立刻打开TESSY的Coverage Analysis视图,定位到未覆盖的判定条件(如value < end中的end计算分支),解释:“这个分支需要start+length溢出才能触发,我们已用TC-04覆盖,但TESSY的MC/DC分析器将end的计算视为独立判定,实际代码中它与value < end是原子操作。”——展现你懂工具原理,不是盲目信报告。

6.3 表3:回归测试基线对比——回答“改了代码后怎么保证没倒退?”

这张表要证明:你的质量是可持续的。每次提交代码,都应有此表更新:

提交哈希日期修改文件MC/DC覆盖率分支覆盖率新增用例失败用例
a1b2c3d2023-10-01range_check.c73.2%85.7%00
e4f5g6h2023-10-05range_check.c73.2%85.7%2(TC-05, TC-06)0
i7j8k9l2023-10-10range_check.c75.1%87.3%00(修复TC-04)

答辩技巧:指着最后一行说:“老师,这次提交修复了TC-04的溢出BUG,不仅让该用例通过,还提升了整体覆盖率。更重要的是,所有旧用例仍100%通过——这证明修改没有引入新缺陷。”然后展示CI流水线截图:“您看,每次push都会自动触发TESSY回归,报告实时更新到这个表格。”

从那以后我每次做嵌入式毕设,都强制走一遍这三张表:先用CTM建表1,再用TESSY跑出表2,最后把CI日志填进表3。不是为了应付答辩,而是让每一行代码都有迹可循、有据可查。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询