Tessy实战:嵌入式单元测试如何满足安全标准与覆盖率要求
简介一份PDF格式的白皮书《Tessy单元测试提高软件质量》面向嵌入式软件工程师、测试人员及关注ISO 26262、ASPICE等安全标准的开发者。内容从单元测试定义与重要性切入说明如何通过早期缺陷检测提升软件质量、降低测试成本并系统介绍测试准备、执行、结果分析与修复的完整流程。重点展开分类树方法Classification Tree Method讲解从识别测试相关方面、分类、重复构建分类树到组合表指定测试用例的步骤同时结合TESSY工具演示单元测试的自动化执行与回归测试。全文为英文共1份PDF文档压缩包大小1.87MB内容结构清晰包含标准要求、实践示例与工具应用说明。已有4857人浏览学习适合正在搭建单元测试流程或引入TESSY工具的团队参考。1. Tessy不是锦上添花单元测试在嵌入式安全标准里是硬门槛做嵌入式功能安全的同行应该都有这个体会绝大多数可靠性事故不是出自复杂算法而是出在一个边界判断、一个数组越界、一个等号。很多团队把IEC 61508和ISO 26262挂在嘴边但真正把单元测试做成每天自动跑的质量门槛的少之又少。Tessy就是干这个的——它把函数级测试从手工打断点、手工填变量变成了一套可重复执行、覆盖率可度量、文档可导出的工程流水线。这份白皮书围绕Tessy讲清楚了单元测试的标准依据、执行路径和用例设计方法适合正在为功能安全认证发愁的嵌入式开发者和测试工程师读完你能知道该测什么、怎么测、坑在哪。2. 单元测试的底层逻辑标准要求、两种执行路径与测试分工2.1 七本标准都在说同一件事函数级测试做汽车、轨交、医疗、航空电子的同学对这套标准不会陌生。白皮书一上来就把标准矩阵列清楚了IEC 61508跨行业功能安全要求模块测试2010版开始要求按SIL等级测覆盖率ISO 26262汽车功能安全明确要求单元测试并且要求用系统化方法识别测试用例Automotive SPICE里叫software units把单元测试作为验证活动的一部分IEC 62304医疗器械软件在5.5章节要求软件单元实现与验证EN 50128铁路管最底层的软件组件叫模块强制测试ISO 13849机械安全在V模型里规定模块测试DO-178B航空要求低层需求测试和软件组件测试根据错误影响决定覆盖率度量深度。你会发现一个有意思的事术语不统一落地到C语言项目里大家测的都是同一个东西——一个C函数。ISO 26262把unit定义为可以独立进行stand-alone testing的最小原子级软件组件在C里就是函数IEC 61508的module定义允许一个模块包含多个函数但实践上大家还是按函数来切。为了避免跟C源文件模块.c文件混淆白皮书统一用单元指代被测函数我认同这个约定——你在Tessy里建测试对象时粒度就是函数级。这一节的意义在于如果你的项目要过认证单元测试不是可选项而是标准的明确要求。很多人以为我们做了集成测试、系统测试就够了实际上认证审核时是会看单元测试记录的覆盖率数据也要跟SIL等级匹配。2.2 原始二进制与测试应用两种执行路径的取舍单元测试的测试对象只要求有这个函数本身但函数要跑起来需要整个可执行环境。白皮书给了两条路。第一条是原始二进制original-binary方式直接用产品最终的完整固件用调试器在函数入口打断点等程序自然走到这里然后手动填充输入、观察结果。它的最大优点是函数在真实运行地址上执行测试环境与最终产品一致也不需要额外编译链接连ROM里已有的旧程序都能拿来测。缺点同样明显自然调用可能等很久有些冷门函数在正常运行里根本不会被调用你得靠翻开关、触发中断这种特殊手段才能让代码走到断点而且被打断点测的单元会真实调用它底下的所有函数底层函数一变上层单元测试就跟着失败排查起来非常头疼。第二条是测试应用test-application方式只拿出被测函数其余缺失的部分main函数、启动代码用测试驱动补上被测函数调用的下游函数用stub桩代替。每个被单元测的函数都有一个独立的测试应用执行环境完全可控。缺点是每个函数都要单独构建如果测试数据全部编译进固件目标板flash不够时塞不下成百上千条用例。我自己的习惯是安全认证项目一律走测试应用方式因为stub隔离之后测试结果可复现回归时不会因为下游函数的改动产生假失败存量系统快速摸底时才用原始二进制方式图个快。维度原始二进制测试应用测试地址产品真实地址测试专用地址额外编译链接不需要需要测试驱动被调函数隔离不可隔离可用stub隔离输入数据量受应用预留空间限制可从host下发展开适用场景存量系统、ROM内程序认证项目、深度回归2.3 谁测开发者测自己代码的盲区与独立测试者的困境白皮书里有一段深有同感开发者不适合测自己写的代码。原因很朴素——如果规范要求的功能压根没实现开发者自己也不会意识到要补这个功能的测试处理意外输入的用例同样容易被忽略因为开发者心里默认调用方不会乱传参。但让不了解代码的人来测难题在于理解接口。测试者拿到一个函数要先搞清楚每个接口变量怎么把抽象输入比如颜色信息的绿色映射成具体值还要判断结果变量里哪个值算正确。这一步的成本极高经常比写测试用例本身还耗时。白皮书给了一种折中方案测试人员基于规范定义抽象测试用例和预期结果交给开发者去执行映射和排错。不过大多数标准要求开发与测试的组织分离这种分工不满足认证要求所以现实中很少见。我的观察是Tessy这类工具真正缓解的是理解接口这一步——接口自动识别后测试者看到的不是光秃秃的参数表而是带类型、带含义的接口视图上手成本低了很多。2.4 自动化是底线为什么所有标准都要求可重复标准要求测试可重复理由很简单改代码或换工具链之后要重跑一遍证明修改没引入新问题。手工测试根本扛不住这种频率所以至少执行环节必须自动化。工具能自动化的其实更多识别单元接口、管理测试数据、构建测试应用、执行用例、评估结果、统计覆盖率、生成文档。这就是Tessy这类工具存在的意义——它不只是帮你跑用例还帮你把测试设计、执行、证据归档整条链串起来。3. 用Tessy跑通一个range函数接口提取、用例构造与bug复现3.1 被测单元一个带隐蔽bug的范围判断函数白皮书给的例子非常典型——判断一个整数是否落在某个区间内。给定区间起始值start和区间长度range_len判断v1是否满足 start v1 start range_len。/* 判断 v1 是否落在 [start, start range_len) 区间内 */ int in_range(int start, int range_len, int v1) { if ((v1 start) (v1 start range_len)) { return 1; /* BUG: 上边界用了 应为 */ } return 0; }这个实现藏着一个非常隐蔽的bug当v1恰好等于start range_len时会被误判为在区间内。如果将来有人用这个函数做阈值判断比如判断传感器读数是否超限这个等号就会把正好在边界上的超限值放过去后果可能是灾难性的。用Tessy测这个函数第一步不是写用例而是让工具从源码自动提取接口。3.2 接口识别TESSY自动从源码提取单元接口Tessy拿到被测函数的.c文件后会做静态解析自动生成单元接口视图。对in_range来说接口包括三个输入变量start、range_len、v1一个返回值int类型以及这个函数调用过的其他函数列表。不需要手工填写参数表这是它比用调试器手工测省时间的关键点。接口视图里每个变量都带类型信息测试者可以在这个视图里直接为每个变量指派测试值。白皮书特别强调一个点Tessy支持抽象测试数据你可以用枚举、符号甚至带业务含义的值来定义输入工具会负责做类型映射。比如start可以输入合法最小值-1这种带标签的值而不是直接填一个无符号整型数字。对独立测试者来说这比翻代码查宏定义再换算成具体数值要友好得多。3.3 构造初始测试用例边界值、等价类与零长度区间接口识别完之后就该设计用例了。对range判断函数我的习惯是先列等价类再补边界值。等价类就三类v1在区间内、v1小于start、v1大于等于startrange_len。边界值要覆盖start、start-1、startrange_len-1、startrange_len另外别忘了range_len0这个特殊场景——此时区间为空任何v1都不应该被判为在区间内。TCstartrange_lenv1预期设计依据TC015351下边界v1 startTC025361区间内部TC035370上边界外v1 startrange_lenTC045340下边界外v1 startTC055050零长度区间范围为空这五个用例里TC03就是专门用来暴露那个等号bug的。在把用例敲进Tessy之前我一般会先用本地脚本快速验证一遍逻辑确认用例本身没问题再上工具。这里用Python的ctypes直接加载编译好的共享库能把同样的用例跑一遍import ctypes lib ctypes.CDLL(./librange.so) lib.in_range.argtypes [ctypes.c_int, ctypes.c_int, ctypes.c_int] lib.in_range.restype ctypes.c_int cases [ (5, 3, 5, 1), # 下边界 (5, 3, 6, 1), # 区间内 (5, 3, 7, 0), # 上边界预期为0实际会误判 (5, 3, 4, 0), # 下边界外 (5, 0, 5, 0), # 零长度区间 ] for start, leng, v1, exp in cases: got lib.in_range(start, leng, v1) status PASS if got exp else FAIL print(fTC start{start:3} len{leng:3} v1{v1:3} fexpected{exp} got{got} - {status})这段脚本的作用是在确认用例设计正确、执行环境无误之后再进Tessy避免把用例写错和被测代码有bug混在一起排查。argtypes和restype用于声明C函数的参数和返回值类型不声明的话ctypes默认按int处理短整型参数可能被截断导致测试结果不可信。运行输出里TC03会显示FAILexpected0 got1这个结果在目标板上用Tessy跑也会是一模一样的失败。3.4 执行测试与覆盖率看见错误比看不见错误值钱用例定好后在Tessy里新建测试工程关联被测函数的源文件选择目标板或模拟器工具会自动编译测试应用并下载。执行完五个用例结果视图里TC03标红其他四个标绿。双击失败的TC03工具会列出实际返回值1、期望值0这时候直接跳到源码看in_range的实现v1 start range_len这个等号就暴露了。修复方式很简单把改成然后重跑整批用例。这一步就是回归测试的雏形——不仅要确认TC03变绿还要确认另外四个用例没被改坏。在这个例子里改动很小但在真实项目里一个边界修复可能影响多个调用路径重跑全量用例是唯一靠谱的验证方式。Tessy在跑完用例后还会给出覆盖率统计。语句覆盖、分支覆盖、MC/DC这些度量白皮书特别指出不同标准按SIL等级要求不同语句覆盖是底线分支覆盖在多数安全等级里是基本要求MC/DC在最高的安全等级比如ISO 26262的ASIL D才强制。别一上来就追求MC/DC 100%先把分支覆盖做扎实再按认证要求逐级加码。4. 分类树方法把测试用例设计从拍脑袋变成结构化表格4.1 为什么需要一个测试用例设计方法第3章里的五个用例靠经验就能列出来但真实项目里的函数不可能都这么简单。一个带十几个输入变量的通信协议解析函数可能的输入组合是天文数字靠拍脑袋列用例一定会漏。标准里其实要求了测试用例识别要有系统化方法但没规定具体用哪种。分类树方法Classification Tree MethodCTM就是白皮书推荐的那套系统化方法。CTM的核心是把测试相关的方面拆出来对每个方面枚举互斥的分类再把这些分类组合成测试用例。步骤就四步识别测试相关方面 → 对每个方面做分类 → 重复细分形成分类树 → 在组合表里指定测试用例。看起来简单但它强制你先把输入空间的结构想清楚再谈选哪些组合这跟直接凭经验写用例是两种思维模式。4.2 对in_range函数建立分类树三个方面、三层结构拿第3章的in_range函数当例子。我先列测试相关方面这个函数本质是在做两个比较v1跟下边界start比v1跟上边界startrange_len比另外range_len本身的值域也值得单独作为方面。第一个方面是v1相对下边界start的位置分三类小于start、等于start、大于start。第二个方面是v1相对上边界startrange_len的位置也分三类等于上边界、小于上边界、大于上边界。第三个方面是range_len自身的取值分三类零、正数、负数虽然不太符合这个函数的语义但作为防御性测试可以保留。这样就形成了一棵三层分类树每个方面下挂互斥的类。组合起来是3×3×327个组合。全组合当然最稳但很多组合是冗余或者不合理的——比如range_len为负时startrange_len可能小于start区间的数学意义就变了。Tessy的分类树编辑器支持在组合表里做约束过滤把不合理的组合删掉保留有代表性的用例。实际保留的用例通常就10到12个比全组合少一半以上覆盖效果却不打折扣。4.3 组合表与测试用例从树到可执行用例的最后一步组合表combination table是分类树的出口。每一行代表一个分类组合对应一条测试用例。工具会把当前选中的类的组合展开成表测试者勾选要保留的组合再为每个组合填写具体的输入值和期望结果。用例v1与startv1与上界range_len说明CT01等于小于正数下边界命中CT02大于等于正数上边界命中bug场景CT03大于大于正数超出上界CT04小于小于正数低于下界CT05任意任意零空区间穿透测试这张表只列了代表性组合完整表里每个方面都要覆盖到。CT02就是TC03的学名版——它来自分类树的结构化推导而不是碰巧想到的边界值。这是CTM的价值就算换个测试者来做只要按同样的方面和分类去拆得到的用例集是稳定、可审计的。白皮书里也提到这类方法并不依赖某个特定工具手工用Excel也能做Tessy只是把树的建立、组合展开、约束过滤这些环节工程化了省掉的是重复劳动不是思考过程。5. 避坑手册Tessy落地时的5个常见翻车点与排查路径5.1 坑一被调函数没有stub回归测试越跑越假现象测试应用方式下被测函数调用的下游函数没有全部打桩第一次跑全绿过两周底层模块改了个内部算法单元测试开始一片红。原因没打桩的下游函数在测试应用里是真实代码它的行为变化直接反映在测试结果上。单元测试变成了准集成测试隔离性被破坏。解决强制要求被测函数直接调用的每个函数都必须有stubstub里只保留最小行为——要么返回固定值要么按最简单的分支逻辑返回。Tessy的接口视图里会列出被调函数清单逐个确认是否已打桩一个都不能漏。从那以后我建测试工程的第一件事就是核对stub清单而不是急着写用例。5.2 坑二测试应用塞不下目标板flash现象几百条测试用例编译进测试应用后下载失败报存储空间不足只能删用例。原因默认把所有测试数据编进了测试应用固件用例一多目标板有限的flash就被撑爆了。解决把测试数据留在host端按用例逐条下发到目标板。Tessy支持这种host/target分离模式缺点是每条用例多一点通信开销但换来的是用例数量基本不受存储限制。量级大的时候这个模式是必选项。5.3 坑三原始二进制方式下被测函数根本跑不到现象用原始二进制方式在函数入口打断点启动应用后等半天断点一次都没触发。原因这个函数在正常运行路径里处于冷区或者触发条件极苛刻自然执行根本走不到。白皮书里也点了这个题可能永远不被调用只能靠翻开关、触发中断这类特殊手段强行拉进去。解决如果确认函数在当前应用里是冷路径果断切换到测试应用方式别在原始二进制上耗时间。原始二进制适合的是存量系统快速摸底这种场景新开发模块做单元测试效率太低了。5.4 坑四覆盖率指标定错level认证审核不过现象团队宣称语句覆盖率100%但认证审核员还是开了不符合项要求补MC/DC。原因不同标准、不同SIL等级对覆盖率的要求不同。ISO 26262里ASIL D对MC/DC有明确要求ASIL A可能语句覆盖就够了DO-178B则按软件等级要求语句、分支或MC/DC。只盯着语句覆盖一个指标跟标准的对齐度不够。解决立项阶段就把标准条款和SIL等级翻出来确定本项目的覆盖率目标是哪一级语句、分支还是MC/DC然后在Tessy里对应配置覆盖率测量项。Tessy会汇总每个函数的覆盖率数据导出报告前逐项核对。5.5 坑五用例变量值映射出错把测试设计错误当成代码bug现象一条用例失败排查半天发现不是代码问题而是测试者把抽象输入映射成了错误的具体值——比如把绿色对应成了枚举里的RED。原因独立测试者不熟悉接口变量的业务语义抽象值到具体值的映射全靠猜。白皮书提到的测试者基于规范写抽象用例开发者执行映射正是为了避开这个坑但组织分离做不到时映射错误非常常见。解决在Tessy里为每个接口变量建立取值字典用带业务含义的别名管理测试值而不是直接裸填十六进制数。用例执行前让熟悉代码的人review一遍取值映射表成本远低于追查一个莫名其妙的失败用例。6. 从单测到质量闭环回归基线、覆盖率门禁与文档自动生成单元测试跑通一批用例只是起点真正值钱的是把它变成可持续的质量闭环。我有三个固定动作。第一建回归基线。第一批用例全部PASS之后冻结测试工程每次代码变更后全量重跑。Tessy会把历史运行结果存下来新版结果跟基线不一致的地方一眼就能看到。这里有个细节不只是失败的用例要查PASS变FAIL要查FAIL变PASS也要查——后者可能意味着你的修复把某个依赖路径改变了只是歪打正着通过了不代表设计意图对了。第二覆盖率门禁自动化。在持续集成里挂一个覆盖率检查步骤低于阈值直接拦下本次合入。阈值按SIL等级定别拍脑袋定个80%。白皮书给的原则很清晰语句、分支、MC/DC按等级选定了就严格执行。我用Tessy导出的XML覆盖率报告做门禁输入脚本里设分支覆盖不低于该函数历史基线防止新增代码悄悄稀释覆盖率。第三文档自动生成。Tessy能导出HTML/XML测试报告包含用例列表、执行结果、覆盖率汇总、stub清单。这些报告直接归档到认证证据包审核时不用临时翻历史记录。给用例命名时按需求ID前缀来比如REQ_123_CT01这样从需求到测试用例到执行结果的追溯链是完整的这是过认证时最省心的一点。用Tessy这几年最大的教训就一句话单元测试的难点从来不是工具操作而是用例设计的系统性和回归执行的纪律性。分类树把用例设计从碰运气变成了结构化推演Tessy把执行和度量从手工变成了自动化但两者都救不了想起来才跑一次的团队。从那以后我每个迭代强制走一遍冻结用例基线、全量回归、核对覆盖率门禁、归档报告雷打不动。希望帮到你。本文还有配套的精品资源点击获取