图覆盖与软件测试充分性:从控制流图到覆盖准则全解析
发布时间:2026/9/15 17:57:59 锦皓数字建站

很多人刚开始学软件测试时对“覆盖率”三个字的第一反应是覆盖率越高测试质量越好。这个想法方向没错但很容易让一个人走进死胡同——因为只看百分比根本说不清楚“缺口到底在哪里”。真正把测试思路打通、让我知道下一步该测什么的是学习图覆盖那段日子。图覆盖不是某个工具一键生成的饼图而是一套描述测试充分性的框架。软件测试里只要谈到白盒测试、结构测试、覆盖准则几乎都会绕到图覆盖这套逻辑上来。这篇是软件测试学习笔记的第二篇我会用尽量少的公式、尽量多的例子把节点覆盖、边覆盖、边对覆盖、主路径覆盖这些基础概念讲清楚再延伸到数据流覆盖最后聊一聊实际工作中怎么选覆盖准则以及初学阶段最容易踩的坑。不管你是刚开始背软件测试面试题还是已经写过一阵子自动化用例、正纠结“用例到底够不够”这篇都适合慢慢读。1. 为什么要学“图覆盖”它和普通覆盖率差在哪1.1 覆盖率最常见的误区行覆盖过了不等于测透了很多团队会在项目里接覆盖率工具比如 Java 项目的 JaCoCo、Python 项目的 Coverage.py、前端项目的 Istanbul。工具报告里最显眼的通常是“行覆盖率”line coverage和“分支覆盖率”branch coverage。我之前见过一个团队某个模块的行覆盖率做到了 95%大家对质量信心很足结果上线后出了一个低级 bug一个elif分支走进去后变量没有按要求更新。问题出在哪看下面这段伪代码def check_grade(score): if score 90: return A elif score 60: return B else: return C如果测试用例只测了 98 分和 50 分行覆盖率其实已经非常高了——每一行都有用例踩过。但score 60为真的那个分支呢没测过。这个分支对应的边在图上是独立的、有方向的它没有被走到。所以“哪些代码被执拗”和“哪些路径被验证”其实是两个问题。图覆盖要解决的正是第二个问题如何把程序变成一张图再基于这张图定义出“测试需求”然后用最少的用例去覆盖这些需求。1.2 程序一旦变成图测试需求就精确了图覆盖里的“图”早期主要指的是控制流图也就是把代码的执行顺序抽象成节点和有向边。但“图”这个概念本身还能延伸到很多场景接口测试里可以把每个接口当作节点调用关系当作边状态机测试里可以把界面或协议状态当作节点事件当作边甚至模块依赖关系也能画成一张有向图。学图覆盖时先抓住控制流图就够了因为它是其他图的简化版也是最容易手工推导的一张图。掌握它的构造方法后你会发现很多覆盖准则其实是通用的节点覆盖、边覆盖、边对覆盖、主路径覆盖这些名字可以套用到任何有向图上。这也是为什么面试官常拿图覆盖来考白盒测试——它不仅仅是代码覆盖率的理论包装而是一个可以跨场景复用的标准框架。2. 从代码到图控制流图的构造方法2.1 切分基本块的三条硬规则控制流图不会为每一行代码单独建一个节点否则图会又大又碎。它的基本单元是“基本块”basic block。一个基本块是一段顺序执行的语句只有一个人口、一个出口。构造控制流图时我用三条规则来切分连续的顺序语句合并成一个基本块遇到条件跳转if、while、switch、三元表达式等时把当前块截断条件判断单独开一个节点遇到可能有异常抛出的地方、函数调用返回点、循环回边等也要考虑是否需要截断块。之所以要用“基本块”而不是“一行代码一个节点”是因为覆盖率计算的本质是判断路径而不是统计单条语句有没有执行。把一连串不可能分支的顺序语句合并成一个节点图的规模会小很多测试需求也更清晰。2.2 用一个三数求最大值函数完整构图我拿一个非常简单的例子走一遍哪怕是零基础也能跟上。def max_of_three(a, b, c): max_val a if b max_val: max_val b if c max_val: max_val c return max_val第一步把顺序语句和分支条件拆成节点。这个函数可以拆成 6 个节点节点编号节点内容n0max_val an1if b max_valn2max_val bn3if c max_valn4max_val cn5return max_val第二步按执行流向连边。从一个基本块出发如果有条件判断就会分出两条边。这个图的边集合是n0 → n1 n1 → n2 b max_val 为真 n1 → n3 b max_val 为假 n2 → n3 n3 → n4 c max_val 为真 n3 → n5 c max_val 为假 n4 → n5这里要特别说一个细节b max_val为假时代码会跳到第二个if所以n1的假分支直接连到n3。很多初学者画图时容易把这个假分支连到别的地方导致后面的测试路径计算全错。2.3 测试路径到底是什么有了图和入口节点、出口节点之后“测试路径”就定义了一条从入口节点到出口节点的节点序列其中相邻节点之间必须存在图中的边。在这个例子里所有测试路径有 4 条n0 → n1 → n2 → n3 → n4 → n5n0 → n1 → n2 → n3 → n5n0 → n1 → n3 → n4 → n5n0 → n1 → n3 → n5这 4 条路径分别对应 4 种真实的程序行为比如第 1 条是b a且c 新最大值第 4 条是两个条件都不成立。到这里“图覆盖”的核心思想已经浮现了既然所有可执行的路径都在这张图上覆盖准则就是“你打算选哪些路径作为测试需求”。选得越严格覆盖得越充分但需要设计的测试用例也越多。3. 基础三连节点覆盖、边覆盖、边对覆盖3.1 节点覆盖最省事的入场券节点覆盖英文叫 Node Coverage简称 NC。它的测试需求是图上所有可达节点至少被一条测试路径覆盖一次。回到max_of_three的例子。如果只要求节点覆盖一个测试用例就够了传(a1, b2, c3)程序一路走 n0 → n1 → n2 → n3 → n4 → n56 个节点全被踩到。节点覆盖是不是太弱了是的。它只能保证“每个基本块都执行过”但完全不管分支方向。比如条件判断里的真分支和假分支节点覆盖根本不区分因为节点只要执行到一次就算覆盖。这会导致什么后果前面check_grade的例子就是很好的说明score 60这个条件本身在图上就是一个节点执行到它并不等于执行了“真分支”和“假分支”两条边。所以在实际项目中纯节点覆盖一般只用来做冒烟测试的底线它最大的优点是成本低、极容易达到最大缺点是真的会漏 bug。3.2 边覆盖把每个分支方向都踩到边覆盖Edge Coverage简称 EC。它对测试需求的定义是图上每条边至少被一条测试路径覆盖一次。这个概念天然解决了节点覆盖的盲区。因为只要每条边都走了每个节点必然也被经过——除非某个节点没有任何入边但这在正常 CFG 里不会出现。max_of_three的边覆盖需要几个用例我试过两个就够了用例一(a1, b2, c1)走路径 n0 → n1 → n2 → n3 → n5。它覆盖了 n1 的真边、n2→n3、n3 的假边。用例二(a1, b0, c3)走路径 n0 → n1 → n3 → n4 → n5。它覆盖了 n1 的假边、n3 的真边。这两条路径合起来边集合 7 条边全部覆盖到了。边覆盖很符合测试人员的直觉每一个判断条件的“是”和“否”都被验证过。很多团队的“分支覆盖率”工具本质上统计的就是边覆盖或近似边覆盖。它是实际使用中最常见、性价比最高的一档。3.3 边对覆盖把连续两条边绑定在一起测边对覆盖Edge-Pair Coverage简称 EPC。它的测试需求是图中每条长度为 2 的路径即连续两条边至少被一条测试路径覆盖一次。为什么要测这么细因为很多 bug 是两个相邻判断条件组合起来才暴露的。比如“登录失败后重试”与“重试仍然失败”是两个相邻分支单个分支分别测没问题但两个分支组合后可能出现锁死逻辑。边覆盖只看单条边抓不到这种组合。在max_of_three里边对覆盖的测试需求就多了。连续两条边的组合有这些[n0, n1, n2] [n0, n1, n3] [n1, n2, n3] [n1, n3, n4] [n1, n3, n5] [n2, n3, n4] [n2, n3, n5] [n3, n4, n5]需要设计测试路径让这些组合尽量都出现。我实际凑出来至少要 3 到 4 个用例。这就体现出测试成本随准则严格程度上升的规律。3.4 三个准则用一张表记住差别准则测试需求粒度max_of_three 需要的用例数直观比喻节点覆盖 NC每个节点至少一次1每个房间都看一遍边覆盖 EC每条边至少一次2每扇门都进出一次边对覆盖 EPC连续两条边至少一次3-4每段相邻的两扇门都连起来走一次4. 主路径覆盖循环出现后测试路径是如何被“收编”的4.1 全路径覆盖为什么在循环面前不可行有人可能会想既然要测充分那就“所有路径都测一遍”不就行了这个想法在处理循环时会立刻撞上南墙。看一个带循环和分支的函数def count_positive(lst): count 0 i 0 while i len(lst): if lst[i] 0: count count 1 i i 1 return count这个函数的控制流图大致是这样的n0count 0; i 0n1while 条件判断i len(lst)n2if 条件判断lst[i] 0n3count count 1n4i i 1n5return count边有n0→n1n1 的真边去 n2n2 的真边去 n3n2 的假边去 n4n3→n4n4 回到 n1n1 的假边去 n5。关键问题在于 n4→n1 这条回边。循环体可以执行 0 次、1 次、2 次……理论上可以是无限多次所以“所有路径”是无限集合。测试需求如果是无限多个那测试就永远做不完了。全路径覆盖在理论上是完备的在实践上是不可行、甚至没有意义的。4.2 简单路径和主路径的判别规则为了把无限路径压缩成有限的、有代表性的集合图覆盖理论里引入了两个定义。简单路径路径中所有节点都不重复的路径。比如 n0→n1→n5 是一条简单路径n1→n2→n4→n1 不是简单路径因为 n1 出现了两次。主路径一条简单路径如果它不能作为另一条更长简单路径的一部分而存在那它就是主路径。换句话说它已经“长到头了”再往前接任何一个节点都会导致重复节点。主路径覆盖Prime Path Coverage简称 PPC就是要求图上每一条主路径至少被一条测试路径覆盖。这相当于什么相当于程序员跑代码时不再追求“把无限种循环次数都测完”而是只关心那些“代表结构特征”的路径循环入口、循环出口、循环体内每个分支、以及它们之间不重复的组合。4.3 主路径覆盖如何落到测试用例上在实际操作中我不会真的列出所有主路径那样太累。我一般会结合循环次数来看循环 0 次、循环 1 次、循环 2 次以及循环内真假分支的组合。这个“0、1、2”方法本质上就是对主路径覆盖的工程近似。拿count_positive举例循环 0 次传入空列表路径 n0→n1→n5验证边界循环 1 次且lst[0] 0传入[1]路径 n0→n1→n2→n3→n4→n1→n5验证正值分支循环 1 次且lst[0] 0传入[-1]路径 n0→n1→n2→n4→n1→n5验证非正值分支循环 2 次传入[1, -1]验证循环回边和不同分支组合的稳定性。真正要警惕的是“不可达路径”。什么叫不可达比如一个函数里先出现if x 10再出现if x 5那么“x 既大于 10 又小于 5”这条路径理论上存在但实际永远执行不到。主路径覆盖不会自动帮你把这些路径剔除覆盖率统计工具也不会理解数据之间的约束。所以我一直建议当覆盖率没到 100% 时先别急着补用例。先把没覆盖到的路径拿出来逐个判断它是真正需要测的行为还是不可达路径。前者补用例后者在测试报告里显式标注“不可行/不可达”理由写清楚。这才是一个测试负责人该有的姿态。5. 数据流覆盖图覆盖的第二层玩法5.1 先把三个词搞清楚def、use、DU 对图覆盖不只可以描述“代码执行路径”还可以描述“数据从产生到使用”的路径。这层玩法叫数据流覆盖。核心是三个概念def变量被定义赋值的位置use变量被使用的位置DU 对某个变量的一个“定义”到某个“使用”的组合。使用又分两种一种是在判断条件里使用叫 p-usepredicate use比如if y 100里的y另一种是在赋值表达式、函数参数、返回值里使用叫 c-usecomputation use比如z y 1里的y。为什么要区分这两个因为 p-use 跟边直接相关——判断条件会导向不同分支而 c-use 通常只影响计算结果不会改变控制流向。5.2 数据流覆盖需求从 def 到 use 的路径数据流覆盖要求在“定义点”和“使用点”之间找到一条路径而且这条路径中间不能再次重新定义同一个变量。这样的路径叫 def-clear path。为什么要强调不能重新定义因为如果中间又给同一个变量赋了值那之前那个定义根本影响不到后面的使用测了也白测。数据流覆盖有三个递增的准则All-Defs每一个定义至少到达一个使用点All-Uses每一个定义到达每一个使用点All-DU-Paths每一个定义到每一个使用点的所有 def-clear 简单路径都覆盖。我用一个简单的费价格函数来展示def calculate(x): y x * 2 # y 的 def if y 100: # y 的 p-use y 100 # y 的第二次 def else: y y 1 # y 的 c-use return y # y 的 c-use第一次赋值的y x * 2它会流向if y 100这个 p-use。如果中间没有任何y被重新赋值那么从 def 到 p-use 的路径是 def-clear 的。但是第二次赋值y 100和后续的return y之间又是另一组 def-use 关系。这种分析的价值在于它能发现普通路径覆盖发现不了的问题。比如某个变量定义了但从未被使用说明代码可能是死代码定义到使用的路径上被重新定义说明前面的定义可能是无效的。图覆盖关注“结构有没有被走到”数据流覆盖关注“这个数据的生命周期里有没有不被察觉的问题”。5.3 数据流覆盖在实际测试里的位置数据流覆盖在白盒测试里常被用来补充单测设计特别是涉及状态变化的代码比如登录状态、订单状态、缓存刷新。它的缺点是分析成本高手工维护很容易出错。所以我不建议所有代码都上数据流覆盖更适合用在风险高、状态多的核心模块。真正在工作中用得更多的反而是把“接口参数”当作变量来分析一个接口入参从接口层传到服务层、再到数据库层中间有没有被意外覆盖、有没有路径导致参数没有生效。这种思路其实就是数据流覆盖在系统测试层面的变体也是软件测试面试里常被包装成“你如何设计参数测试”的高阶题。6. 准则那么多实际项目怎么选包含关系与取舍6.1 包含关系谁包含谁不是拍脑袋图覆盖的各个准则之间不是毫无关系它们存在包含关系。这里的包含是指如果一个测试路径集合满足某个较强准则的测试需求那么它往往也会满足较弱准则的测试需求。常见的包含关系可以这样表示节点覆盖 NC ⊆ 边覆盖 EC ⊆ 边对覆盖 EPC ⊆ 主路径覆盖 PPC数据流覆盖这边也有类似关系All-Defs ⊆ All-Uses ⊆ All-DU-Paths这个“⊆”的含义是满足后者通常也满足前者。比如满足边覆盖一般就隐含满足节点覆盖满足主路径覆盖一般就隐含满足边对覆盖。这条链最有用的地方在于帮助定目标当你设定的较高准则遇到困难可以往下降一档并且仍能保留大部分覆盖保障。反过来如果项目安全等级高就要往上升。关系弄清楚以后就不会出现“列了一大堆用例连边覆盖都没达到”的情况。6.2 不同场景下的建议选择根据我的经验选准则要看三点代码风险、运行频率、维护成本。第一类是快速回归测试比如每次提交代码都要跑的 CI 流水线。这种环境追求快选节点覆盖或边覆盖就足够用例多了跑一遍几分钟甚至十几分钟团队根本承受不住。第二类是核心业务模块比如支付、订单、权限校验建议用主路径覆盖把关键分支和循环边界都设计进去。第三类是曾经出过严重 bug 的模块或者状态转换极其复杂的模块这时候可以考虑数据流覆盖尤其是 All-Uses 级别成本比 All-DU-Paths 低不少效果已经很好。要特别点醒的是覆盖率不是越高越好。为了把一个分支从 95% 顶到 100%可能要写很多复杂用例还要处理大量不可达路径。与其在边缘地带死磕不如把省下的时间用来测更重要的业务链路。覆盖率是一个衡量手段不是测试目标这个顺序不能搞反。7. 学习过程中我踩过的坑和一条可行的练习路线7.1 三个容易误解的点第一个误解是把“行覆盖”当成“路径覆盖”。行覆盖只能告诉你某行代码执行了没有不能告诉你它是通过哪条路径执行到的。同一个节点可能由多条不同路径到达但行覆盖报告里它们都算“被执行”这会掩盖不同路径组合的问题。第二个误解是忽略不可达路径的筛选。工具报告覆盖率不到 100% 时很多人的第一反应是补用例结果补了半天覆盖率纹丝不动。我之前遇到过一段历史代码里面有个条件永远是矛盾的两条路径中有一条根本不可能成立工具一直报红的正是它。后来我把这个路径标注成不可达问题才算解决。第三个误解是认为“图覆盖只适用于白盒测试”。图覆盖的抽象能力完全可以迁移到黑盒和灰盒测试比如把接口调用链画成图用主路径覆盖去设计端到端用例把用户状态机画成图用边覆盖检查状态迁移是否齐全。我后来做接口测试时还会刻意画一张“业务状态接口动作”的有向图效果比凭经验列用例要稳得多。7.2 一套自测练习法从图形构建到覆盖计算如果你正在自学图覆盖并且觉得光看概念记不住我建议你按这套路线练一遍。第一步找一个几十行的纯函数要求里面至少有一个循环和一个嵌套分支最好能处理列表或数组。网上找面试题库里的算法题就很合适。第二步不看答案自己手动画出控制流图。画完后再拿工具对比比如 Python 的pyflow或者自己写一个简单的转换程序来验证。不要小看这一步很多错误都发生在基本块切分上要么少拆了一个分支头要么把else的连边画错。第三步为这张图分别计算节点覆盖、边覆盖、边对覆盖的测试需求然后设计测试用例去覆盖它们。你可以用代码覆盖率工具来验证达到 100% 的边覆盖时是不是正好对应你设计的那几条路径。第四步加上一个循环尝试用“0 次、1 次、2 次”的思路去设计主路径覆盖并把不可达路径标出来。这一步能极大锻炼你的路径感知能力。我强烈建议把这个练习过程记录成笔记因为它会为后续学习数据流覆盖、变异测试打下一个特别好的基础。7.3 面试官问图覆盖怎么组织回答最近很多准备软件测试面试的朋友问我面试官问“你怎么保证测试是充分的”该怎么答。其实图覆盖就是一个很标准的回答框架。我会这样组织先说自己会先用控制流图梳理被测函数的执行结构然后用边覆盖或主路径覆盖来定义测试需求再根据需求设计用例之后借助覆盖率工具做量化验证遇到未覆盖的路径时区分“真正需要测”和“不可达路径”最后把结果沉淀成测试设计和复盘报告。这个回答既展示了理论基础又体现工程经验比干巴巴说“覆盖率 100%”要可信得多。最后再分享一个小技巧我自己学图覆盖时最大的一次认知升级发生在画完第一张带循环的控制流图之后。当时我盯着那个环看了很久突然明白覆盖率工具为什么总是“差一点到 100%”因为公司不会为一个不可能的路径浪费测试成本而在报表里把不可达路径标清楚才是真正专业的表现。如果你现在刚入门别着急把所有准则都背下来先把一段简单代码从“源码”转成“图”再手工算一次边覆盖。这个过程走通了后面再难的概念都只是换一层皮而已。图覆盖这套思维值得你在学习软件测试的路上多花点时间。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。