ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

VectorCAST结构覆盖率测试实战:从插桩到MC/DC覆盖

VectorCAST结构覆盖率测试实战:从插桩到MC/DC覆盖 1. 为什么做结构覆盖率测试以及为什么选用VectorCAST1.1 结构覆盖率到底是什么为什么不能只看功能测试做嵌入式软件测试的朋友应该都有体会功能测试过了代码合入后回归也绿了但产品上线后偶发故障仍然出现。很多问题本质上是“代码里有一段逻辑根本没被执行过”而普通功能测试很难发现这一点。结构覆盖率测试解决的就是这个盲区——它量化的是“被测代码被执行的程度”而不是“功能对不对”。所谓结构覆盖率核心是统计分析程序在测试执行过程中哪些语句被执行了、哪些分支被走到了、哪些条件组合被验证了。它有几个经典维度语句覆盖率、分支覆盖率、条件覆盖率、MC/DC修正条件判定覆盖Modified Condition/Decision Coverage、路径覆盖率。这些维度层层递进对代码路径的验证强度依次提高。比如语句覆盖率只统计每行代码是否执行分支覆盖率则要求每个判定条件的真/假分支都至少走到一次而MC/DC在安全关键领域里更严格要求每个条件独立地影响判定结果。为什么不能只靠功能测试因为功能测试关心的是“需求是否被满足”它天然是从外部输入出发的。但代码内部可能存在大量的防御性分支、错误处理分支、边界条件分支这些分支在正常功能路径中根本不会被触发。再加上嵌入式代码里充满大量的if-else嵌套、switch-case、逻辑运算组合任何一个分支没走到潜在的缺陷就留在那里。结构覆盖率测试就像是给代码拍X光片把“有没有被验证过”这件事暴露出来。我个人的感受是结构覆盖率测试是连接“需求验证”和“代码验证”之间的桥梁。需求验证告诉你功能符合预期代码验证告诉你代码本身的质量下限。没有覆盖率数据支撑的测试报告说服力是不够的尤其是在安全认证、质量审计、交付评审这种场合。1.2 VectorCAST在覆盖率测试上的独特优势VectorCAST是我用过的嵌入式软件测试工具里做结构覆盖率测试最顺手的之一。它来自Vector公司原来叫Vector Software主打嵌入式环境的单元测试、集成测试和覆盖率分析。它解决的关键问题可以概括成三点适配嵌入式交叉编译环境、自动化插桩、代码覆盖率与用例管理一体化。第一嵌入式环境适配。很多通用覆盖率工具比如GCOV在PC端很好用但在嵌入式交叉编译环境下配置起来很麻烦尤其是你用的编译器不是GCC、目标平台是特定单片机或汽车级芯片时GCOV基本没法用。VectorCAST自带编译器适配机制支持大量主流交叉编译器可以对接目标机执行也能做基于模拟器的宿主测试。这一点对嵌入式项目来说太关键了。第二自动化插桩。VectorCAST会在编译阶段自动在源代码中插入覆盖率探针不用手工改造代码也不污染源代码仓库。你要做的只是配置被测文件列表和覆盖率类型剩下的插桩、编译、链接、收集数据全部自动化。这在动辄几十万行代码的嵌入式工程里省下来的工作量非常可观。第三覆盖率与用例闭环。它不只是告诉你覆盖率是百分之多少还把“哪一行没执行”“哪个分支没走到”和“需要什么样的测试用例才能覆盖”关联起来。你可以基于覆盖率分析结果直接补充测试用例再执行、再分析形成闭环。这个体验比你先跑一个覆盖率工具、再到另一个工具里手写用例的流程顺畅得多。另外热词里提到VectorCAST单元测试教程和静态测试我在这里也多说一句VectorCAST本身提供了完整的单元测试能力包括桩函数、测试用例自动生成、测试环境搭建等而结构覆盖率测试通常是嵌在单元测试流程中的一环。静态测试则侧重于代码规则检查和覆盖率测试在质量保障链条上各管一段但VectorCAST的测试框架可以让两者在同一套工程里协同工作。2. 测试环境准备与工程搭建要点2.1 环境评估编译器、目标平台与许可证动手建工程之前先花半天时间把环境参数理清楚能省掉后面大多数无谓的折腾。我做的第一个VectorCAST覆盖率项目就因为在编译器配置上想当然导致后面每个用例都编译失败排查了整整两天才意识到是编译选项和原工程不一致。环境评估主要看四件事硬件平台、交叉编译器、编译选项集、执行方式。硬件平台决定了你最终在什么设备上跑测试。常见的有三类宿主PC、评估板/开发板、Hardware-in-the-Loop台架。宿主PC上跑速度最快适合前期功能验证和用例调试目标板上跑贴近真实运行环境适合确认编译器行为差异和最终覆盖率收集台架则是系统级验证阶段的事。覆盖率测试建议尽量在宿主环境先跑通一遍再用目标环境做最终确认这样迭代效率最高。交叉编译器是VectorCAST环境配置的重头。你需要确认编译器完整路径、版本号、支持的C/C标准、编译选项是否带自定义宏。有一个容易被忽略的坑如果你的项目使用了编译器自带的设备抽象层比如很多芯片厂商的SDK插桩后的代码可能需要额外的链接参数这些参数在构建系统里是自动加上的但VectorCAST工程里需要手动配置。许可证方面VectorCAST的授权方式有节点锁Node-Locked和浮动许可Floating License。覆盖率分析功能通常包含在单元测试模块授权里。如果公司只有一两套浮点许可建议在CI服务器上配置好许可池避免团队成员抢授权。编译选项的收集也很重要特别要注意这几类预处理器宏比如#define ENABLE_DEBUG、头文件搜索路径、架构相关的编译参数比如-mcpucortex-m4。这些参数如果不一致最典型的问题就是插桩后的代码编译不过或者更隐蔽的代码行为和原工程不一样。提示建议在项目根目录下维护一份vectorcast_env.md记录编译命令、关键宏定义、头文件路径和编译器版本每次新建工程时直接参照这份文档配置能减少大量重复排查时间。2.2 创建工程与添加被测代码的核心步骤在VectorCAST中新建覆盖率测试工程的流程我通常按下面这几步走。不同版本界面名称会有差异但整体逻辑是稳定的。第一步打开VectorCAST选择新建工程。工程类型选择“VectorCAST/Cover”或者直接进入“VectorCAST/Unit”选择测试级别然后指定工作目录。工作目录建议和源码目录分离避免测试生成的中间文件污染源代码版本库。第二步配置编译环境。在Project Settings或Environment配置页里选择工具链Toolchain输入交叉编译器的路径设置标准版本、优化级别。这里要特别提醒覆盖率测试不推荐开启高优化级别。优化级别高时编译器可能会合并基本块、删除冗余条件导致覆盖率统计失真。你可以做一个小实验同一个函数分别在-O0和-O2下跑覆盖率两者的分支覆盖率数字经常对不上那不是工具的Bug是编译器改写了代码结构。所以覆盖率测试建议至少单独为它配置一套-O0或者-O1的编译参数。第三步添加被测源文件。在工程里新增源文件Add Files可以选择单个文件也可以批量添加。对覆盖率测试而言我建议先选被测函数所在的核心源文件不要一上来就把整个工程几十个文件全部扔进去否则首次构建慢、编译错误多排查起来也麻烦。等主流程跑通了再逐步补充周边文件。第四步执行构建。构建成功后VectorCAST会自动为每个被测函数生成驱动代码、测试脚本和覆盖率数据文件。这个过程在GUI里点Build按钮即可也可以用命令行执行vcast -b命令方便集成到CI流程里。第五步保存工程。新建工程后第一次构建成功一定先保存一次快照。方便后续测试用例调整、覆盖率分析时能够回到稳定的基线版本。这个习惯我在项目中被救过好几次——有一次在批量修改用例时误删了一个关键测试组直接回滚到快照省了一下午重写工夫。注意构建成功不等于环境完全没有问题。务必在构建后查看编译日志重点确认两份内容一是插桩日志确认探针数量是否符合预期二是链接日志确认没有“undefined reference”类错误。不要在Compiler Warnings里放过任何一行警告嵌入式代码的警告往往预示着移植性问题。关于静态测试的补充如果你同时需要做静态检查VectorCAST中可以在工程属性里启用静态分析模块对源码做规则检查。它和覆盖率测试共用同一个工程模型切换成本低。我在实际项目里通常先跑静态测试改完一轮代码规则问题后再做动态覆盖率测试这样动态测试阶段的编译错误会少很多因为很多问题静态阶段已经提前暴露了。3. 覆盖率类型选择与插桩设置3.1 语句、分支、MC/DC……覆盖率类型怎么选VectorCAST支持的覆盖率类型比较全面常见的包括语句覆盖率Statement、分支覆盖率Branch、条件覆盖率Condition、MC/DC覆盖率、路径覆盖率Path、调用对覆盖率Call Pair等。不同类型对测试充分性的度量粒度不同消耗的插桩资源和运行开销也不同。我用一个生活的类比来解释。语句覆盖率就像“你检查一个车间里每个工位有没有人操作过”只要工位有人站过就算覆盖但不关心操作者的动作对不对分支覆盖率则要求“每一扇门都从里外各推开一次”每道开关门都要测过条件覆盖率再进一步“门上的每个锁点都要单独验证”MC/DC则要求“每个锁点的开关状态都能独立影响门能不能打开”路径覆盖率要求“每个工位之间所有可能的通行顺序组合都走一遍”。实际选型时我的建议是根据项目安全等级和代码风险等级分层选择普通业务逻辑、安全等级低比如非ASIL等级的常规控制语句覆盖率加分支覆盖率达标线一般定在语句100%、分支90%以上。安全关键模块如车辆底盘控制、医疗设备控制逻辑至少做到语句、分支、条件全覆盖推荐做到MC/DC 100%。航空电子类项目DO-178C DAL A/BMC/DC属于强制要求覆盖率数据要完整留存形成追溯链。具体到VectorCAST的操作在工程设置里找到Coverage配置页勾选需要统计的覆盖率类型。需要注意同时勾选的类型越多插桩探针数量越多编译时间和运行开销也越大。嵌入式目标机上如果RAM和Flash资源紧张过量的探针可能导致代码放不下。我碰到过一个项目只做语句覆盖率时代码容量占用从62%升到81%翻上加分支覆盖后直接爆了Flash最后只能调整插桩级别把探针分配到子单元级别才把影响控制住。下表是我常用的覆盖率类型选择参考覆盖率类型度量对象适用场景插桩开销语句覆盖率每行可执行语句代码自测、常规项目低分支覆盖率每个判定分支一般功能模块中条件覆盖率每个条件真/假值复杂逻辑模块中高MC/DC每个条件独立影响结果安全关键模块高路径覆盖率判定组合路径极小但关键的函数极高提示实际项目中很少需要做到路径覆盖率全量覆盖路径是随判定数量指数增长的一般只对少量关键函数单独做路径分析不适合全工程铺开。3.2 插桩配置里的关键参数插桩Instrumentation是覆盖率测试的核心机制。VectorCAST的插桩策略是在源代码中插入覆盖率探针函数每个探针对应一个覆盖率事件。配置插桩有两个关键参数插桩粒度Instrumentation Level和探针数量上限。插桩粒度有三个级别函数级、文件级、单元级。函数级粒度最小探针最少但覆盖率统计比较粗文件级通常够用单元级最适合做精细分析但探针多。我的经验是初期用文件级粒度跑全量分析聚焦到某些率值始终上不去的模块时再单独给这个文件配置单元级插桩细化分析。探针数量上限在VectorCAST中是一个可配置项。目标机RAM有限时这个值必须谨慎设置。如果工程过大、探针数量超过了上限需要拆分成多个子工程分别跑覆盖率。拆分的原则是“以被测函数为单位”保证每个子工程内部的函数间调用关系完整避免跨工程的桩函数干扰覆盖率统计。还有一个容易被忽略的参数插桩阈值Coverage Threshold。这个参数决定工具体自动报告的覆盖率达标线比如设置语句阈值100%、分支阈值90%那么报告里低于这条线的单元会被自动标记为未达标。这个功能在交付审计阶段特别有用可以快速筛出所有不达标的模块批量生成问题清单。插桩后的代码在编译选项里我建议加上-fno-omit-frame-pointer这类选项便于在覆盖率数据异常时配合调试器定位。另外如果源代码里包含汇编文件要注意VectorCAST默认不对汇编文件插桩汇编段需要通过其他手段比如代码审查加手工打点来覆盖。4. 测试用例设计与覆盖率收集过程4.1 用例设计让覆盖率“跑”起来的思路覆盖率不是“测出来的”而是“设计出来的”。没有针对性地设计用例覆盖率只会停留在随机命中的水平。我在项目里总结了一套比较实用的设计流程第一步先梳理被测函数的逻辑结构。把函数内的所有判定点、分支条件、循环边界、异常分支在代码里标出来。这一步相当于“覆盖率靶点清单”后面每个用例都能对应到这条清单上的若干靶点。第二步用等价类与边界值方法生成基础用例。等价类保证每种逻辑行为至少有一个用例触发边界值则重点覆盖循环边界、数组下标边界、数据类型极值。比如一个if (count 0 count MAX)的判断基础用例至少要包括count0、count1、countMAX-1、countMAX四个点。第三步针对未覆盖分支进行定向用例补充。第一次执行完用例集后立即查看覆盖率报告找到未覆盖语句和未经过分支逐条分析为什么没有被执行到。大多数情况是缺少某个特殊输入少数情况是代码本身存在死代码Dead Code。区分这两类问题的方法很直接如果通过输入可以构造出触发该分支的条件就是用例不足如果无论如何输入都触达不到那就要怀疑代码逻辑是否冗余。关于自动生成用例VectorCAST支持从函数原型自动生成“基础测试用例”包括零值、极值、典型值等。这个功能适合作为用例集的起点但不要指望它能覆盖全部分支。原因很简单自动生成没有结合编码上下文很多模块级的状态依赖它无法感知。我通常是自动生成打底手动设计补全这样效率和质量比较平衡。还要说说桩函数Stub设计。嵌入式代码里被测函数通常会调用底层硬件驱动比如寄存器读写、外设接口、OS服务。在宿主环境下这些函数不存在必须用桩函数代替。桩函数的设计原则是只提供被测函数当前需要的交互不引入过多业务逻辑。举例来说如果被测函数需要从一个传感器读数接口获取温度值桩函数可以简单返回一个可控的模拟温度值配合测试用例参数设置就能模拟不同温度场景。4.2 执行测试与覆盖率数据合并用例设计完成后在VectorCAST中执行测试有两种方式GUI中点击Execute按钮批量执行命令行里用vcast -e指令执行整个测试套件。调试阶段建议用GUI逐条跑方便看每个用例的入参出参和覆盖率增量覆盖率收敛阶段改用命令行批量执行输出执行日志和覆盖率报告。执行过程中一个很实用的操作是“单次执行覆盖率增量观察”功能。VectorCAST允许你针对某个用例集单独查看它的覆盖率增量也就是这个用例新增覆盖了哪些语句或分支。这功能非常有用因为你可以直观看出每个用例的“贡献度”找到那些贡献度很低的冗余用例随手删掉让用例集变得精简高效。覆盖率数据合并的场景主要出现在多测试工程、多轮执行的情况下。比如你把一个模块拆成两个子工程分别跑最终需要将两份覆盖率数据合并成模块级的总体覆盖率。VectorCAST提供覆盖率数据库合并功能可以把多个工程或多次运行的数据叠加在一起。合并策略有两种并集合并和交集合并。项目验收时用并集因为我们要证明的是“所有必须覆盖的代码总共有多少被执行过”。合并操作有个注意事项不同版本代码的覆盖率数据不能直接合并。如果两次执行之间源码有改动探针ID集会变化合并出来的数据是错乱的。所以覆盖率数据要跟源码版本一一对应建议把每次覆盖率分析对应的源码版本号记录下来合并前先核对版本信息。这个我踩过坑有一次我把两个子工程的覆盖率报告合并没注意它们基于的代码版本不同结果合并后总覆盖率超过100%被审核人员质疑工具配置有误解释了半天才说明白。5. 覆盖率报告解读与不达标处理策略5.1 报告怎么看从总体统计到未覆盖代码定位执行完测试后VectorCAST会生成多份报告HTML格式的覆盖率汇总报告、源码级标注报告、函数级明细报告等。报告里的几个关键指标我的读法是这样的总体覆盖率看全局是否达到目标线。但不要太依赖这个数字因为全局数字会被低复杂度模块“平均”掉比如一个简单的小函数100%覆盖可能掩盖另一个复杂函数只有40%覆盖的问题。函数级覆盖率把每个函数的覆盖率从高到低排序优先看排名靠后的函数。这才是真正需要花精力处理的地方。文件级覆盖率判断代码整体质量分布某些文件覆盖率异常低往往意味着对应的需求文档缺失或测试设计不充分。源码级标注报告是我最常用的。它会用不同颜色在源码上标注每一行语句是否被执行、每一个分支是否被经过。红色标出来的就是未覆盖的“死角”。分析未覆盖代码时我习惯分三类处理第一类是输入可达但用例没构造好。这种情况最普遍比如某个case分支没有对应的测试输入。解决方法是补充用例。第二类是输入可达但依赖前置状态。典型例子是函数内部根据某成员变量是否初始化来决定走哪个分支但测试用例没有预置该状态。解决办法是在用例中增加前置状态设置或桩函数配置。第三类是真正的死代码。这种情况要谨慎不要轻易判定是“不需要的代码”就直接删。如果它确实不会被任何输入触发合理的做法是在代码评审中确认是删除还是保留设计。如果保留需要在报告中注明“经确认该分支为防御性代码无需覆盖”。5.2 覆盖率“差一点”时的几个实用手段实际项目中覆盖率最尴尬的阶段是“差一点”比如分支覆盖率停在93%、MC/DC停在97%。这时候蛮干地补用例往往事倍功半我总结几个实用手段第一利用反向追踪定位未覆盖条件。VectorCAST支持从某个未覆盖分支点往回追溯找到到达该分支所需的条件组合。如果能手工推导出这个组合直接构造对应输入就行如果组合特别复杂比如多个条件依赖可以把这个未覆盖分支单独做一个焦点分析工程屏蔽掉其他函数集中精力构造用例。第二利用数据文件批量构造边界值。如果你的被测函数参数多手工逐条构造输入很慢。我建议写一个简单的脚本生成CSV格式的边界值数据再通过VectorCAST的数据驱动测试功能批量导入。比如被测函数的参数有范围限制脚本自动生成最小值、最大值、中间值、负数、零、字节溢出等组合一条命令就能生成上百个用例覆盖效率立刻提升。第三适当的桩函数“松绑”。有些分支走不到是因为桩函数返回的值范围太窄。比如桩函数模拟一个AD转换假如你固定返回整数10被测函数里大部分和阈值比较的分支永远不会触发。这时候可以把桩函数改造成可配置模式通过用例参数控制返回值的不同区间。这只是测试桩的“灵活度”调整不是放宽覆盖率定义两者要区分清楚。注意覆盖率不达标时千万不要通过“删减被测代码”或者“屏蔽未覆盖分支”这种手段来凑达标率这是审计红线。合规的做法只有两条要么补充测试用例要么对无法覆盖的代码做正式评审并给出书面结论。另外有的项目在最后冲刺阶段会用“排除代码”功能将一些不用做覆盖率统计的代码如启动代码、空函数、死代码标记为排除项。这个功能可行但排除理由要写得足够扎实每一行排除代码都能在审计时解释得通才行。6. 常见问题排查与实操心得6.1 高频问题速查表我在多个VectorCAST覆盖率项目里遇到过的问题整理成一个速查表按出现频率排序问题现象常见原因处理办法插桩后编译报错编译选项与原工程不一致对比构建日志补齐宏定义、头文件路径覆盖率始终显示0%测试程序未真正执行到被测函数口检查测试环境的入口函数设置确认被测函数被调用某个分支永远覆盖不到前置状态未设置/桩函数返回值范围受限检查桩函数配置增加状态预置用例覆盖率数据合并后异常源码版本不一致/探针ID偏移核对版本重新生成被测代码快照目标机执行超时探针过多导致运行缓慢降低插桩粒度拆分测试分组用例执行通过但覆盖率无增量被测函数被桩函数替代而非真实调用检查测试配置里的“Stub该函数”选项是否被误勾选链接时出现重复定义多个子工程重复包含同一个源文件核查工程文件列表去重后重建工程报告里找不到某函数函数为静态函数且未被导出在插桩配置中手动添加该函数为被测入口最典型的一个坑是“分支覆盖不到其实是桩函数惹的祸”。我经历过一次被测函数有一段代码依赖某个外部函数的返回值来决定是走正常流程还是错误处理流程。外部函数被我写成了永远返回零值的桩导致错误处理分支永远不执行覆盖率一直卡在80%上下。最后排查到这一层才发现不是用例不够是桩函数给的值范围根本触发不了另一个分支。把桩函数改成一个可配置值的状态机后覆盖率直接拉满。另一个容易踩的坑是测试环境与构建环境的优化级别不一致。沿用-O2优化编译后的代码许多分支会被编译器优化掉覆盖率的对象就不是你写的源代码了。所以我一再强调覆盖率测试要用独立的-O0或-O1环境并且在这个环境上做好配置记录。6.2 几个让我少踩坑的经验经验一覆盖率测试要尽早介入。很多团队是在项目交付前一个月才想起补覆盖率这时候代码已经冻结补用例的成本高到离谱。我在项目里推的是“每完成一个功能模块就同步做该模块的覆盖率基线”。这样到集成阶段覆盖率数据是跟着代码演进的每个模块都有独立的覆盖率记录不用堆到最后一起算。经验二把覆盖率测试写进CI流水线。利用VectorCAST的命令行接口把单元测试、覆盖率统计集成到每日构建里。每天定时跑一次生成覆盖率趋势图。趋势图上如果某个模块的覆盖率突然下降立即能反查到当天代码变更是不是引入了大量未覆盖分支这个反馈速度比月底集中检查快得多。经验三重视代码评审里的覆盖率视角。我发现很多未覆盖分支其实是代码评审阶段就能发现的问题。比如评审时看到一个大函数好几百行各种复杂嵌套就应该提问这个函数的测试用例设计了吗分支复杂度这么高覆盖率靠什么保证这样把覆盖率压力前置到设计阶段比事后补救健康很多。经验四文档记录要同步。VectorCAST生成的覆盖率报告建议和测试用例、源码版本一起归档。归档时我会加一个覆盖率报告阅读说明写清楚三个信息代码基线版本、工具版本、覆盖率类型定义。这个文件看起来不起眼但在外部审计时省掉了大量重复说明工作。最后再说一个细节覆盖率测试的“完成”标准很多人理解为“100%全绿”但更专业的表达是“已定义覆盖目标全部达成且未覆盖项均有评审结论”。这两种表述在审计中的分量完全不同。前者适合内部自驱后者才是能对外交付的证据链。建议团队从一开始就按“覆盖率目标例外处理”的模式来管理覆盖率数据到了认证和交付阶段会省心非常多。
返回列表