简介:面向嵌入式软件测试与质量保障从业者的单元测试学习资料,聚焦如何借助Tessy工具与分类树方法提升软件质量,适合具备一定C语言与嵌入式开发基础、希望系统掌握单元测试流程的工程师与测试人员。压缩包内共1个文件,为1份PDF白皮书,约1.87MB,篇幅精炼、结构完整,便于直接阅读与检索。已有4876人学习下载,具备较好的参考热度。资料从单元测试的定义、适用时机与执行人员等基本问题切入,结合Tessy实例演示被测单元接口、初始测试用例设计、测试执行、问题定位及回归测试的完整链条;随后系统讲解分类树方法的用途与操作步骤,包括识别测试相关方面、分类、重复分类构建分类树,以及在组合表中指定测试用例。白皮书还提及IEC 61508、ISO 26262、Automotive SPICE等标准对模块测试与单元测试的要求,并给出支持分类树方法的工具概览,可帮助读者建立从标准要求到测试用例设计的实践思路,适合作为嵌入式单元测试入门与方案参考。
1. 一个五行 C 函数,把单元测试的决策点全暴露了
一个只做区间判断的 C 函数,五行代码,三个参数,逻辑简单到可以口算。我拿它当单元测试的入门素材,是因为它一次踩中三件事:边界写错、用例靠拍脑袋、回归时没人敢改。这份白皮书用 Tessy 把一个带 bug 的函数从接口识别一路跑到回归测试,再用分类树方法把「想不全的用例」变成一张可核对的组合表。它不教语法,也不谈工具选型,讲的是安全关键嵌入式软件里单元测试的落地路径:标准到底要求什么、原始二进制和测试工程怎么选、桩函数什么时候必须上、集成测试接不住的那部分该由谁兜底。适合正在被覆盖率报告追着跑、又说不清用例为什么这么写的固件工程师。
2. 把标准条文落到一行 C 函数:单元、接口与黑盒边界
2.1 模块测试还是单元测试:用词不同,落到 C 语言只剩一个函数
不同行业的软件安全标准对这件事的称呼并不统一。跨行业的功能安全标准 IEC 61508 用「模块测试」,2010 年的版本开始建议按安全完整性等级测量代码覆盖率;汽车行业的 ISO 26262 用「单元测试」,并且明确列出了应当用来识别测试用例的方法,同时推荐采用 IEC 61508 里的覆盖率度量;面向过程的 Automotive SPICE 用「软件单元」,把单元测试作为验证的一部分;医疗设备软件生命周期标准 IEC 62304 在第 5.5 步要求软件单元的实现与验证;轨道交通的 EN 50128 把最底层软件组件叫模块,要求逐个测试;机械安全的 ISO 13849 在 V 模型里同样规定模块测试;机载的 DO-178 用低层需求测试和软件组件测试,并按错误影响等级要求覆盖率测量。
名称差异背后是两套定义。ISO 26262 把单元定义为「原子级软件组件……可以被独立测试」,IEC 61508 把模块定义为「由过程和/或数据声明构成的构造」。把这些定义套到 C 语言上,最终落到实处的测试对象是同一个东西:一个 C 函数。IEC 61508 的模块定义允许多个函数放进一个模块,那只是组织方式上的松紧差别,不改变「测什么」。所以白皮书干脆统一用「单元」指代 C 函数,避免和 C 语言里的源文件模块混淆,后面所有关于测试对象的讨论都指向函数。
有一点所有标准都咬得很死:测试计划、测试文档,以及重复执行此前成功测试的能力,也就是回归测试。这三样东西决定了单元测试不能靠人肉记忆维持,它必须是可重放、可追溯的。
2.2 单元接口就是测试用例的骨架
单元测试要尽可能完整、尽可能隔离地测一个函数,测的是它对着规格的接口。这是一次黑盒测试,函数内部怎么实现不是决定因素。接口的构成要拆细:函数读取的变量集合是输入,函数写入的变量集合是输出或结果变量,函数有返回值的话返回值也属于结果,函数调用的其他函数在广义上同样属于接口的一部分。
变量可以一脚踏两边。单元内部对一个变量做自增,这个变量就同时是输入和输出,测试用例里必须为它准备一个初值、同时给出一个期望终值。这一点在新手写用例时最容易漏,只写了输入没写期望,跑出来的结果就变成了「看看等于几」而不是「应该等于几」。
接口的形状直接决定了测试用例的结构。一条测试用例由两部分构成:输入变量的具体取值,以及在用这些输入执行时你期望输出变量呈现的取值。后面这半句是用例的灵魂,期望值只能从规格推导出来,不能从实现里读出来。从实现里抄期望值,等于用代码证明代码,测了个寂寞。
拿白皮书里的例子说:一个判断整数值是否落在[start, start + len)区间内的函数,输入是起点、长度、待判值三个整型参数,输出是布尔返回值。这个接口小到一张表就能写完,但它包含的取值组合足够把边界条件、空区间、负数、加法溢出这些坑全部摆出来。
2.3 哪些单元值得单独测:复杂度、调用频次与安全等级
单元测试的价值首先体现在它把测试复杂度压下来了。一个处在调用链高位的函数,你要构造出能覆盖某个底层函数错误处理分支的用例,可能得绕过十几层判断,把整条链路的状态都摆对。更麻烦的是,如果调用方始终正确地使用被调方,被调方的错误处理代码在集成测试里几乎永远走不到——调用方永远传合法参数,被调方那段if (ptr == NULL)就是死代码。隔离测试把这个问题绕开了,被调函数被桩替换,错误分支可以在桩里强行触发。
输入到输出的映射越复杂,单元测试越划算:排序、数据分析、复杂决策这类函数,一条用例就能验证一整段计算逻辑,投入产出比很高。挑这类单元可以用 McCabe 圈复杂度作为筛子,阈值各家不同,但思路上把复杂度最高的那批函数先排进测试计划,比按文件平摊用例数量要有效得多。
应用里被高频调用的基础函数也值得优先测。它错一次,影响面覆盖整个产品,而修一次的成本又极低。
单元测试也有它答不了的问题,最典型的是单元之间的时序交互。两个函数单独测都过,组合起来因为执行顺序或者共享状态出错,这类问题必须交给集成测试,别指望在单元层兜底。我见过把单元测试当万能药的团队,最后是在集成阶段被时序问题打回原形的。
安全关键单元天然是单元测试的首选对象,这不用论证:标准要求、失效后果、可追溯性三条都指向它。
2.4 开发自测还是测试代测:一个能跑起来的折中
开发者不适合测自己的代码,这个结论在行业里没什么争议,尤其是实现完整性和「意外输入」这两块。原因很直白:如果开发者漏实现了规格里的某个功能,他大概率也不会想到去测这个不存在的功能,因为他脑子里那版实现就是完整的。
让外部测试者来做也有成本。经验数据是,测别人写的代码,大部分时间花在搞懂接口上:一个传「颜色」的参数,在实现里对应什么具体数值,结果变量取什么值才算对,都得先摸清楚才能写期望值。这个熟悉过程的时间往往比执行测试本身长得多。
折中办法是分两段。由没写过这段代码的人按规格设计用例,输入用抽象数据表达(颜色就写「绿」),期望结果也按规格写;用例集交给开发者,由他把抽象数据绑定成具体值(绿 → 具体的 RGB 数值)后逐条执行。用例失败,开发者就地定位、必要时改实现。执行测试因此变成软件处理流程里的一步,地位接近编译器检查语法——代码写完顺手跑一遍,红了就改。
这条路和部分标准要求的开发与测试组织分离并不完全吻合,实践中也不常见。但在人手紧张的项目里它确实能跑起来,前提只有一个:用例的期望值必须来自规格,一旦发现用例是照着实现改出来的,这套流程就失效了。
3. 原始二进制还是测试工程:两种跑法的取舍与桩的边界
3.1 原始二进制跑法:不重编译,但调用时机不由你定
一种做法是直接把最终产品用的完整程序拿来跑,也就是完整翻译、链接、分配之后的产物。执行环境和手工测试很像:用调试器加载程序,在待测函数入口下断点,启动,命中断点后把输入数据写进对应变量或内存区域,继续跑,函数返回时读结果。程序回到断点之后,可以立刻接着跑下一条用例。
这条路的优点很实在。被测单元运行在它将来真实所在的地址上,测出来的行为和产品一致;不需要额外的翻译和链接开销,因为用的就是原始程序;如果目标板上的程序已经烧进 ROM,照样能做单元测试,这种情况下甚至连编译器都不需要,有一台调试器就够了。
缺点也很实在。第一,正常运行时多久才会调用到这个函数,不由你决定。有些函数在产品流程里可能永远不被调用,比如只在异常路径上出现的那段代码,这时得临时加一个调试入口、拨一次开关或者触发一次中断,才能把它执行到。第二,原始二进制跑法没法给被调函数打桩,除非做特殊处理,这意味着被测单元每次执行都会真的走到它调用的所有函数。后果是应用重新发布之后,某个根本没改过的单元测试突然失败,查到最后发现是别处改动引起的副作用,这种血泪经验在多人协作的项目里非常常见。第三,输入数据的量受限制,单元如果通过指针接收数据,你只能喂进目标机为这块缓冲区预留的那么多字节,多一个都没有。
3.2 测试工程加桩:用隔离换可复现
另一种做法只从原程序里取被测单元,其余部分单独提供。让程序能跑起来所必需的那些东西——main()、启动代码——在白皮书里被叫作测试驱动,它们和被测单元一起链接成一个专供测试的可执行程序。每个被测单元都有自己的专用测试工程。
理想情况下,被测单元调用的函数全部用桩替换。这就是可复现性的来源:被测单元的输入输出关系被彻底隔离,它调用谁、返回什么、什么时候返回,全部由你在桩里写死,不受其他模块当前状态影响。同一个用例今天跑和三个月后跑,结果必须一致,否则回归测试没有任何意义。
桩不是随便写个空函数返回 0 就完事。返回值要按规格里的契约给,输出参数要按契约写,桩内部还可以顺手记录调用次数和入参,用来验证「该调两次的只调了一次」这类行为约束。我在项目里一般会让桩在收到非法入参时直接断言失败,这比让它静默返回一个默认值有用得多——静默返回会把接口误用掩盖成一次「通过」的测试。
这条路要付出的是编译链接成本,每换一个被测单元就得重建一次测试工程。自动化工具的价值主要就体现在这里,手工维护几十个测试工程的 makefile 是不可持续的。
3.3 测试数据放目标机还是放主机:存储与通信的权衡
测试工程可以直接把测试数据带在身上,程序一加载,数据就跟着进了目标机,后续执行不再需要主机参与。用例数量少的时候这是最省事的做法。用例数量上去以后会撞上存储上限:数据段跟着用例数量线性增长,测试工程可能根本链接不进目标板的存储空间。
另一条路是数据留在主机,执行到哪条用例就下发哪条,只有当前需要的那份数据进入目标机。存储压力基本消失,代价是每条用例多一次通信开销。在存储紧张的板子上,这个交换通常值得做;在通信链路很慢的台架上,就得算一下时间账。这个选择没有标准答案,取决于你的板子还剩多少 flash、你的调试通道有多快。
3.4 一张表定方案,以及为什么自动化不是可选项
| 维度 | 原始二进制 | 测试工程 + 桩 |
|---|---|---|
| 是否需要重新翻译链接 | 不需要 | 每个被测单元一份 |
| 测试对象所在地址 | 产品最终地址 | 测试工程分配的地址 |
| 被调函数 | 全部真实执行 | 可用桩替换 |
| 别处改动的影响 | 可能引起用例无故变红 | 隔离,基本不受影响 |
| 触发待测函数的难度 | 取决于产品流程,可能永远调不到 | 测试驱动直接调用 |
| 指针入参的数据量 | 受目标机缓冲区限制 | 由测试工程或主机下发决定 |
| 对编译器的依赖 | 可以不需要,仅需调试器 | 需要 |
| 典型场景 | 已烧录固件、快速抽查 | 常规单元测试、回归集 |
选型逻辑可以简化成两句话:临时排查、验证一个已经烧在产品里的老函数,走原始二进制最快;要建可持续的回归集,必须走测试工程加桩。两者不是互斥的,同一个项目里混用很常见。
最后一点必须单独说:自动化不可省。所有标准都要求测试可重复,理由也很清楚——被测对象或者开发环境一改,此前跑过的测试就得重跑一遍。手工测试做不到这件事,时间成本高到没人愿意执行。工具在这里承担的工作远不止「跑一下」:识别接口、管理测试数据、生成测试工程、执行测试、判定结果、统计覆盖率、生成测试文档,这一串动作里任何一环靠手工,回归频率就会被压到每月一次甚至更低。而每月一次的回归,等于没有回归。
4. Tessy 实操:从接口识别到回归测试的完整链路
4.1 被测函数与它自带的越界判断 bug
白皮书给的例子是一个区间判断函数:给一个起点和一个长度确定数值范围,判断另一个整数是否落在这个范围内,规则是start <= value < (start + length)。
/* range_check.c —— 判断 v 是否落在 [start, start+len) 这个左闭右开区间内 */ typedef unsigned char BOOL; #define TRUE 1 #define FALSE 0 BOOL range_check(int start, int len, int v) { int end = start + len; /* 上界,规格里是开区间 */ if (v < start) { return FALSE; /* 小于下界,不在区间内 */ } if (v <= end) { /* BUG:这里应是 v < end,<= 把开区间变成了闭区间 */ return TRUE; } return FALSE; }逻辑说明:函数先算上界end,然后分三段判断——小于起点直接判否,小于等于上界判是,其余判否。参数含义分别是:start区间起点(含),len区间长度(应保证非负),v待判值;返回值TRUE表示在区间内。
实现里藏着一个明显的错误。规格是左闭右开,v == start + len时不该被算进区间,但代码用了<=。手工推演一条:start = 5, len = 2, v = 7,end = 7,v < 5不成立,7 <= 7成立,函数返回TRUE;而规格要求返回FALSE。这类 bug 在代码评审里也经常被放过,因为这段代码读起来「很正常」,错误只体现在一个符号上。
4.2 自动识别接口与把测试数据抽象出来
Tessy 会解析被测单元的源码,自动确定接口:函数的形参、返回值,以及单元读写的全局变量。对range_check来说,识别结果是一张很短的接口表:
| 接口元素 | 方向 | 类型 | 说明 |
|---|---|---|---|
| start | 输入 | int | 区间起点,闭区间端点 |
| len | 输入 | int | 区间长度,规格要求 >= 0 |
| v | 输入 | int | 待判值 |
| 返回值 | 输出 | BOOL | TRUE 表示落在区间内 |
工具把这张表填出来,省掉的是手工录入的时间,真正费脑子的活在后面:把测试数据从抽象层落到具体值。规格里描述输入时用的是抽象语言,比如「一个明显小于起点的值」「一个恰好等于上界的值」,而目标机上跑的必须是4、7这样的具体数字。这个映射关系(抽象值 → 具体值)一旦写错,用例的语义就变了:本来想测「上界外」,结果喂进去一个区间内部的值,测试照样绿,bug 照样在。
我的习惯是把抽象值和具体值的对应关系单独记一份,跟用例一起纳入版本管理。换一个 target、换一种整型宽度,只需要改映射表,用例本身的语义不用动。这也是分类树方法能跨项目复用的前提。
4.3 分类树方法:把用例设计从经验变成组合表
白皮书后半部分讲的分类树方法,解决的是「用例想不全」这个最普遍的问题。它分四步走:识别与测试相关的方面(aspects),对每个方面做分类(classes),对分类再继续分类,递归下去得到一棵分类树,最后在组合表里挑组合,指定成一条条测试用例。
对range_check,我一般会拆出四个方面,每个方面下面的分类如下:
| 方面 | 分类 |
|---|---|
| v 相对区间的位置 | v < start;start <= v < end;v == end;v > end |
| len 的取值 | len = 0;len = 1;len > 1 |
| start 的符号 | 负;零;正 |
| 上界计算 | start + len 不溢出;start + len 溢出 |
四个方面全组合是 4 × 3 × 3 × 2 = 72 条。全部跑完当然最彻底,能发现方面之间的交互缺陷,但成本摆在那里,实际项目里很少这么干。工程上的做法是先保证每个分类至少被覆盖一次,这叫最小覆盖,再对高风险组合补全——这里的「高风险」很明确,就是边界位置和溢出这两个方面交叉出来的那一小块。
关键在于,覆盖情况是可见的。上面这张表一画出来,哪一格还没被任何用例踩到,一眼就能看出来。这比对着代码拍脑袋想「好像测过了」强太多,也是分类树方法相对随机设计的核心价值:它把用例设计的完备性从主观判断变成了可核对的矩阵。
组合表挑出来的用例如下(期望值全部从规格推导,不看实现):
| 用例 | start | len | v | 期望返回 | 覆盖的分类组合 |
|---|---|---|---|---|---|
| TC01 | 5 | 2 | 4 | FALSE | v < start;start 正 |
| TC02 | 5 | 2 | 5 | TRUE | v == start,左闭 |
| TC03 | 5 | 2 | 6 | TRUE | 区间内部 |
| TC04 | 5 | 2 | 7 | FALSE | v == end,右开——暴露 bug |
| TC05 | 5 | 2 | 8 | FALSE | v > end |
| TC06 | 5 | 0 | 5 | FALSE | len = 0,空区间 |
| TC07 | -3 | 2 | -2 | TRUE | start 为负,区间跨零 |
| TC08 | 0 | 1 | 1 | FALSE | start 为零,len = 1,v 落在上界 |
TC06 值得多说一句:len = 0时区间是空的,任何值都不该判为在内,期望返回FALSE。但看实现,end = start,v == start时会走v <= end分支返回TRUE——同一个<=错误在空区间上又犯了一次,只是表现更隐蔽。
溢出那一格要单独处理。start + len在int上做加法,结果超出表示范围就是未定义行为,指望它「自然回绕」是在赌编译器。合格的从业者一般会在实现里加显式检查,或者在设计上就用无符号类型限定取值范围,然后在分类树里把「溢出」这一格标记为「由设计约束排除」并写明理由。分类树里留一个空格子不写理由,过半年就没人知道它是漏了还是有意为之。
4.4 执行、判定与回归
用例录进工具之后,执行流程是:生成测试工程,下载到目标机或仿真环境,逐条执行,工具自动把实际输出和期望值比对,不一致就标红。
覆盖率在这一步同步产出。语句覆盖、分支或判定覆盖、以及 MC/DC,按项目要求选。这里有个心态问题需要摆正:覆盖率是用来反查用例设计的,不是用来交差的。看到判定覆盖 100% 就收工,等于放弃了分类树已经帮你标出来的那几格空白。
TC04 会红,这是预期内的。定位过程不需要调试器单步跟进,工具已经把输入、期望输出、实际输出三列摆在一起,一眼就能看出是边界符号写反了。把v <= end改成v < end,重新生成测试工程,把八条用例全部再跑一遍——这一步就是回归测试:不只确认 TC04 变绿,还要确认 TC01 到 TC03、TC05 到 TC08 没有因为这次修改而变红。
修完之后,这八条用例就该冻结下来,成为这个单元的回归基线。以后任何人改这个函数,先跑这八条。白皮书里给的顺序是「初始测试用例 → 执行测试 → 排错与回归测试」,这个顺序不能颠倒:先把用例和期望值定下来再执行,否则执行结果会反过来影响你对期望值的判断,那就不叫测试了。
5. 避坑:单元测试落地最容易翻车的五处
5.1 边界值靠猜,覆盖率 100% 仍然漏 bug
现象:覆盖率报表很漂亮,语句 100%、判定 100%,但产品里还是出现了区间越界的问题。
原因:覆盖率度量的是「哪些代码被执行过」,不度量「取值空间被采样得多完整」。一段if (v <= end)只要有一个v < end的输入走进去,这个判定分支就算覆盖了,bug 依然纹丝不动。range_check就是活例子:TC03 一个用例就能让分支覆盖率很好看,但真正能暴露问题的 TC04 得专门设计。
解决:边界值不能靠灵感,要靠分类树显式列出来。每个边界至少配两条用例——恰好落在边界上、刚刚越过边界,成对出现。像v == start和v == end这种点,必须单独成为一个分类,而不是被「区间内部」这个分类顺手带过。
5.2 没打桩,一个无关改动让一组用例集体变红
现象:应用重新发布之后,一批跟本次改动毫无关系的单元测试突然失败。改回去,又全绿了。
原因:走的是原始二进制跑法,被测单元执行时会真的调用它依赖的所有函数。别处改了某个被调函数的返回值或者副作用,被测单元的行为跟着变,用例自然红。这种问题最消耗排查时间,因为失败点看起来在 A 单元,根因在 B 模块。
解决:建回归集的时候一律走测试工程加桩,把被测单元和它的依赖隔离开。确实需要验证真实依赖行为的,单独做一组集成级别的测试,不要混进单元回归集里。原始二进制跑法留给临时排查用,别拿它当日常回归的主通道。
5.3 指针入参被截断,测试「通过」是假通过
现象:一个通过指针接收数组的函数,测试全部通过,但在产品里处理长数据时出错。
原因:原始二进制跑法下,喂给指针的数据量受目标机为这块缓冲区预留的容量限制。你喂进去 16 字节,函数「处理完了」,测试判定通过,实际上后面 200 字节根本没进来。这种通过比失败更危险,因为它给了你一个错误的安全感。
解决:凡是涉及指针入参的单元,测试工程里要显式传入长度,并在桩或者包装层里校验长度是否被篡改。数据量大的场景改成主机侧按需下发,别指望目标机的静态缓冲区。用例设计上,长度参数本身也要进分类树:零长度、单元素、超过缓冲区容量。
5.4 测试数据全烧进目标机,用例加到一定数量链接失败
现象:测试工程一开始跑得好好的,用例加到一两百条之后,链接阶段直接报存储空间不足。
原因:测试数据跟着测试工程走,数据段随用例数量线性增长。目标板的 flash 就那么大,迟早撞墙。
解决:数据留在主机,执行到哪条用例下发哪条,目标机里只保留当前用例需要的那份。代价是每条用例多一次通信开销,但换来的是用例数量不再受存储限制。如果调试通道特别慢,退一步的做法是按功能分组拆成多个测试工程,分批跑。
5.5 用例跟着实现写,漏实现的功能永远测不到
现象:测试全绿,上线之后用户报「某个规格里写明的功能根本没有」。
原因:用例是看着实现写的,实现里没有的分支,用例里也不会出现。开发者漏实现了规格中的某个功能,他自己不知道,写用例时自然也不会去测这个不存在的东西。这是单元测试里最隐蔽的一类失效,因为所有指标都正常。
解决:把用例设计和用例执行拆开。用例从规格和分类树出发,先写期望结果,再写输入;写用例的人最好不是写这段代码的人。退一步,至少要求每条用例都能追溯到规格里的某一句话,追溯不上的用例要么补规格、要么删掉,别让它悬在那儿当摆设。
6. 覆盖率数字之外的验证习惯:三条检查线
覆盖率报表交给谁都能看,但真正决定这套东西能不能长期跑下去的,是三个不那么显眼的检查动作。
第一条检查线是拿覆盖率反查分类树,而不是拿它当验收标准。我的做法是每次跑完回归,把没被覆盖到的判定分支单独列出来,然后回到分类树里找:这个分支对应哪个方面的哪个分类?如果分类树里有这一格,说明是用例漏了,补用例;如果分类树里压根没这一格,说明是设计维度漏了,补方面。这两种情况的处理方式完全不同,混在一起看就永远补不齐。
第二条检查线是冻结不稳定输入。单元测试最怕的是「今天绿明天红,重跑又绿」。常见来源是时间、随机数、以及没有被桩隔离的外部状态。常见做法是在桩里把时间固定成一个常量,随机数用固定种子,任何读硬件寄存器的接口一律打桩返回预设值。这一条不做,回归集三个月内一定会退化成没人看的红点墙。
第三条检查线是「红-绿」校验,也就是新增的回归用例必须在修复之前是失败的。这条最容易被跳过,因为大家习惯是修完 bug 顺手补条用例,跑绿了事。但一条从来没红过的用例,你无法证明它真的测到了那个 bug——它可能测的是别的东西,也可能什么都没测。我的习惯是用一段很小的脚本把这件事固化成提交前的闸门:
#!/usr/bin/env bash # 回归闸门:新增用例必须先在未修复代码上失败,修复后才允许通过 set -euo pipefail CASE_ID="$1" # 新增用例编号,例如 TC04_end_boundary # 第一步:在修复前的代码上跑这条用例,期望它失败 if run_unit_tests --filter "$CASE_ID"; then echo "用例 $CASE_ID 在修复前就通过了:它没有测到目标缺陷,重写" exit 1 fi # 第二步:切到修复后的代码,跑全量回归,确认没有连带破坏 run_unit_tests --all这里的run_unit_tests是对测试工具调用的封装,不是某个工具的固定命令,常见做法是在 CI 里包一层脚本,把过滤执行和全量执行分开。关键是第一步的语义:用例在缺陷存在时必须红。这一步拦住过我好几次「看起来补了其实没补」的用例,尤其是边界那条——写完发现它在修复前也是绿的,一查是把v == end写成了v == start。
从那以后我每次提交缺陷修复,都强制先跑一遍新增用例、确认它是红的,再去改实现。这个动作花不了两分钟,但它保证了回归集里每一条用例都有存在的理由。希望帮到你。
本文还有配套的精品资源,点击获取