测试设计
测试用例决定了测试套件能捕获什么。 之后的一切——测试框架、测试运行器、 测试计数——都只是执行那个决定。
本技能替代的时刻
| 正要… | 改为… |
|---|---|
| 编写快乐路径并就此停止 | 遍历输入空间:对每个参数,找出其等价类和边界 |
| 从实现推导测试用例 | 从规范推导,并让实现给你惊喜 |
| 使用舒适值进行测试 | 测试每个边界内部的最后一个值和外部的第一个值 |
| 测试规范命名的边界 | 每个范围有两个端点;句子遗漏的那个仍然是边界 |
| 枚举规范列出的角色 | 添加调用者遗漏的角色:无角色、未认证、未分配 |
| 从快乐路径变化一个条件 | 构建决策表并覆盖两个条件同时失败的规则 |
| 绘制状态表然后继续 | 在每一行放入测试ID;最需要的是写着“不允许”的那行 |
| 编写“拒绝,或接受前提…”作为预期结果 | 每个用例一个结果;无法决定的规范是一个开放问题,不是测试 |
| 因不太可能而跳过错误路径 | 每个错误模式都是一个等价类:无效输入、缺失资源、拒绝、超时、半完成 |
| 留下未实现的路径,没有测试、跳过标记或无人断言的桩代码 | 编写测试断言该路径今天以未实现的方式被拒绝;它现在通过,并在有人构建它的那一天变红(见下文) |
| 通过主流程测试一个功能 | 从用例中读取替代流程和异常流程;每个都是一个等价类 |
| 通过阅读来评估测试套件 | 对照规范计数:等价类、两个边界、规则、转换 |
| 向已覆盖的等价类添加第五个测试 | 将其用于未覆盖的等价类——第五个测试消耗运行时间且不增加覆盖率 |
攻击规范,而非实现
通过阅读代码编写的测试断言代码的行为。 它到达时即通过,它将当前bug编码为期望值, 并且变更后的失败意味着“代码改变了”, 这已是已知信息。 这是可测量的,而非怀疑:在24个Java仓库上, LLM生成的预言倾向于捕获程序的实际行为而非其预期行为(Konstantinou, Degiovanni, Papadakis, arXiv:2410.21136)。
从事物应该做什么推导——规范、 线格式、合约——然后才运行它。 唯一的合法例外是有意固定当前行为的特征测试, 并且必须标记清楚,以免后来者将固定的bug误认为需求。
一个有用的味道:如果每个测试在首次运行时都是绿色的, 那么测试套件是从实现推导的,无论意图如何。
等价类:计数类,而非测试
代码无法区分的输入形成一个类, 每个类一个测试就足够了——第二个到第五个消耗运行时间且不证明任何新东西。 这种纪律双向适用:没有类未覆盖, 没有类被意外覆盖两次。
一个包含十七个测试的套件看似全面。 但它是否全面取决于这十七个测试落入多少个类中, 而这个数字不出现在任何运行器的输出中。 必须手工对照输入空间计数——这正是测试计划的作用。
规范列出的类是容易的一半。 一个三行的角色表有第四个类, 即不匹配任何行的请求, 而这是从表构建的套件从未包含的。 对于每个参数,在编写任何用例之前,先编写无效类与有效类并列: 错误类型、缺失、超出每个列出的值、 完全没有权限的调用者。
教学大纲将此作为规则而非习惯陈述: 在等价划分中,覆盖项是分区, 100%意味着每个已识别的分区都得到练习,“包括无效分区”, 并且分区“不得重叠且必须是非空集”(ISTQB CTFL v4.0.1 §4.2.1)。 对于多个参数,它命名了最弱的标准 Each Choice——每个参数的每个分区一次, 忽略组合——这是大多数套件达到的级别而不知其名, 也是下面的决策表开始的地方。
边界,因为错误就在那里
差一错误是最常见的算术错误,
并且在除边界外的任何地方都不可见。
在每个边界处最重要的对是边界值及其跨过线的最近邻居,
即2值BVA(CTFL v4.0.1 §4.2.2);
3值BVA增加了内部邻居,
教学大纲展示了这带来了什么:
一个本应为x ≤ 10但写为x = 10的检查,10和11都通过,
只有9能捕获它。
反复出现的边界:空、正好一个、多个;零和负数;
声明的最大值及其后一个;
恰好等于缓冲区、限制、超时的值。
来自本机器的一个具体对: 在8,190字符处正常的PATH检查必须在8,192字符处失败, 必须在这两个值处测试。 在4,000和12,000处通过几乎不能说明两者之间的线。
当边界是声明的前置条件时,
越过边界值是调用者的bug而非有预期结果的用例;
design-by-contract决定给定边界属于哪种情况,
测试随后断言适用的一种。
规范未命名的边
“最多10 MB”命名了一个范围的一端。 另一端是否存在取决于句子是否提及它, 而从文本推导的用例在文本停止处停止: 一个在−1、0和+1字节处测试了上界且没有低于1,024的套件, 只对半个范围进行了边界分析。 定义没有偏向性: “一个分区的最小值和最大值是其边界值”(CTFL v4.0.1 §4.2.2)。 首先为每个分区写下两个边界, 然后每个边界与相邻分区中的最近邻居一起处理。 未命名的边通常是零、空或一, 并且规范通常对此保持沉默,因为无人决定。
同时移动两个条件
通过从快乐路径改变一个条件构建的套件,覆盖了每个条件的每个值,但几乎不覆盖任何组合: 格式错误、尺寸过大、数量过多,每个单独出现, 而没有两个同时失败的用例。 当两者都适用时哪个错误获胜,也很少在规范中, 因此缺失的用例隐藏了一个缺失的决策。
将条件布局为决策表并计算规则,
然后覆盖两个条件同时失败的规则,以及角色限制和格式限制同时适用的规则。
在决策表测试中,覆盖项是包含可行组合的列,
100%意味着每个这样的列都得到练习(CTFL v4.0.1 §4.2.3);
教学大纲补充说表“有助于发现需求中的任何缺口或矛盾”,
这正是上面缺失的决策自行浮现的地方。
同一份文档在 Each Choice 下可能显示为完全覆盖,而在列下仅覆盖四分之一,
只有第二个数字说明了一些关于组合的情况。
当规则数是标志集的乘积时,
更便宜的修复在上游:
state-first将标志折叠为可能发生的状态,
表也随之缩小到这些状态。
表是对测试套件的声明
状态表、决策表、用例的流程列表: 每一行都声明存在一个针对它的测试。 将测试ID放入行中。 没有ID的行是未覆盖的等价类, 而写着“不允许”的行是最可能为空的行, 因为编写禁止感觉就像已经处理了它。 禁止的转换——在飞行中发送第二个——就是测试, 而应该阻止它的守卫就是被测试的东西。
状态表使这一行显式化: 无效的转换是一个空单元格, 而教学大纲定义的三个标准中最强的, 所有转换覆盖率, 要求尝试每一个转换——每个测试用例一个, 以便一个缺陷不能掩盖另一个(CTFL v4.0.1 §4.2.4)。 有效转换覆盖率,即大多数套件停下的0-switch, 永远不会触及这些单元格, 这就是未测试的“不允许”行如何保持绿色。
然后对照阅读两份文档。 一个测试的路径与表不一致(表说 失败→发送中,测试走 失败→已选择→发送中)意味着其中之一是错误的, 除了比较之外没有其他方法能找出哪个。 自转换和离开失败状态的转换也算作行。
每个用例一个结果
“拒绝,或者如果接受,发送后不留下损坏的数据”无法通过或失败。 它诚实地出现,当规范从未说明零字节文件会发生什么,且作者拒绝发明答案时, 由于用例明显存在,它通过了审查。 析取隐藏在预期列中。 正确形式是向规范所有者提出的问题, 如此列出,并且在得到回答之前不测试该类。
错误路径也是等价类
不快乐的路径是未测试代码积累的地方, 因为在正常使用中没有东西练习它们——锁持有者中途死亡、 不存在的映射文件夹、 缺失预期字段的输入。 每个都是一个等价类,与任何输入范围一样有权获得测试。 一个仅证明工具在一切配合时工作正常的套件,测试的是演示, 而不是工具。
用例在代码存在前枚举等价类
用例是已经切割成等价类的规范:一个主流程、 每个替代流程、每个异常流程、 以及每个可进入的前置条件。 编写它的人已经完成了本技能要求的枚举——替代和异常流程就是错误路径——所以一个练习了主流程的套件只覆盖了文档列出的N个类中的一个。
从用例中读取流程并为每个分配测试, 然后检查前置条件, 因为一个流程只有在其前置条件可持有时才可达。 翻译页面的奇偶检查有一个主流程(两种语言编辑并提交)、 一个替代流程(声明为例外的一侧编辑)、 以及两个只有在前置条件写下后才出现的异常流程: 树是脏的,所以无法判断陈旧性; 历史被重写,所以声明的提交不再可解析。 两者在一个站点上一周内被命中。 仅快乐路径不会显示任何一个。
异常流程是否真的会发生,不能由其感觉多不可能来决定。
操作范围说明了什么超出范围,危险分析说明了什么仍然存在;
两者都存在于assurance-case中,
本技能采用它们产生的列表,而非凭直觉修剪。
覆盖率是命名标准下的数字
同一个测试文档有三种不同度量:100%的条件覆盖率、90.9%的MC/DC、
25%的决策规则覆盖率。
所有三个对同一文件都为真,
所以没有标准的覆盖率数字毫无意义,
而在计数后选择标准的数字意义更小。
在计数前命名标准,
使用教学大纲自己的术语,以便数字可验证:
分区覆盖率和 Each Choice(§4.2.1),
2值或3值BVA(§4.2.2),决策表列(§4.2.3),
所有状态、有效转换或所有转换(§4.2.4)。
MC/DC不在基础教学大纲中;
它来自安全标准(DO-178C, ISO 26262),
将其与其他并列而不说明,是一种混合引用,看起来像严谨。
数字欠读者的其余内容在measured-claims中。
检查测试套件:计数,不要判断
询问测试套件是否充分,会返回产生它的盲点。
同一模型上的第二次会话共享这些盲点,
同一个人第二天也会分享。
教学大纲称此为“一定程度的独立性”,
四个级别中的第二个,并说明了价值来自何处:
“作者和测试者之间认知偏差的差异”(CTFL v4.0.1 §1.5.3)。
同一模型上的第二次会话共享偏差并只保留标签。
不共享偏差的步骤是计数:
列出规范中的等价类并勾选有测试的,
列出每个范围的两个边界,列出决策表的规则,
列出每个状态表的每一行,
并引用每个缺口的文件和行号。
预期值是否正确仍然是一个判断,并且留给未编写它的读者;
等价类是否有测试是算术。
如果计数是抽样而非穷举,
报告说明是抽样。
判决的默认反驳形式在adversarial-verify中;
本技能提供要计数的内容。
当请求移除一个等价类时
“双重发送不可能发生,因为按钮已禁用,删除那行”基于一个层中的一个守卫的强度,从套件中移除一个等价类。 按要求做,并用一句话说明哪个等价类现在未覆盖以及什么能抵达它: 状态机仍然接受来自任何不是按钮的调用者的第二次发送。 带那句话删除的行是决策; 不带那句话删除的行是永远无法再次找到的缺口。
未实现的路径有测试来表示
故意留在此次变更之外的路径、
此版本不支持的角色、尚未解析的格式、
抛出NotImplementedException或调用todo!()的分支,
仍然是输入的一个等价类,
测试套件记录今天在那里发生什么。
编写测试断言该路径以未实现的方式被拒绝:
它引发的异常或错误值、
缺失的菜单项、回答“不支持”的请求。
那个测试现在通过,并完成两项工作。 它将缺口放在测试套件的列表中, 阅读等价类的读者可以找到它, 而不是在注释或工单中。 并且它在有人实现该路径的那天变红, 那是必须编写其真正用例的时刻; 失败的测试就是提醒,它被替换而非删除。
跳过标记或待定测试不是
…(已截断以进行翻译)