用遗漏用例检验已经通过的智能体测试
本文目录
当智能体同时编写代码和测试时,全绿结果只回答了一个有限的问题:在那次运行中,这个实现是否满足了那些断言?判断变更时,还要问:这些断言能否拒绝一种看似合理、却违反需求的实现?
先写下一条可以独立于代码阅读的规则。然后选择一个输入,让正确实现与一种看似合理的错误实现必然给出不同结果。与泛泛要求“提高覆盖率”相比,这个遗漏用例更有用。
先写规则,再读测试
虚构需求 LIST-3 要把变更文件列表格式化为显示文本:
- 输入是非空显示标签字符串组成的数组,标签中不含回车或换行。
- 每个输入项精确产生一行:短横线、空格、未经修改的标签,再加 LF。
- 保留给定顺序和重复标签。不要排序、去重或丢弃后面的项目。
- 空输入产生空字符串,没有标题,也没有空行。
这些是原创教学需求,不是从某个仓库或文件名约定中推断出的规则。任意文件系统名称和无效输入类型,都不在这个示例的输入契约内。
下面是正确的小型实现:
export function correct(files) {
return files.map(name => `- ${name}\n`).join('');
}
两个不同的错误替代实现如下:
export function firstOnly(files) {
return files.length ? `- ${files[0]}\n` : '';
}
export function sortedUnique(files) {
return [...new Set(files)].sort().map(name => `- ${name}\n`).join('');
}
firstOnly 丢掉了后面所有条目。sortedUnique 保留了一个看起来像样的列表,却改变了顺序,并移除了重复条目。任何测试执行之前,根据 LIST-3 就能看出它们的问题。
对三个实现应用同样的弱检查
在这段代码中,format 表示当前被审查的实现。本地运行器会把相同的四项检查应用到每个实现。
import assert from 'node:assert/strict';
// W1: empty output has no malformed bullet lines.
assert.ok(format([]).split('\n').filter(Boolean)
.every(line => line.startsWith('- ')));
// W2: one familiar item works.
assert.equal(format(['src/app.js']), '- src/app.js\n');
// W3: a multi-item result contains the first familiar name.
assert.ok(format(['src/app.js', 'src/view.js'])
.includes('src/app.js'));
// W4: a stub returns its hard-coded response.
const stub = () => '- src/app.js\n';
assert.equal(stub(['src/app.js']), '- src/app.js\n');
对筛选后的空列表,W1 是空真:没有任何一行能够违反该谓词。它不能确立精确的空输出要求。W2 从未检查后面的项目。W3 只检查某个名称是否存在,没有检查完整且有序的结果。W4 根本没有调用 format;一个在别处有用的桩测试,不能仅凭返回自身预期值,就为这个格式化函数提供证明。
Node 的带版本标签的断言文档 介绍了严格断言模式和相等性检查。严格比较可以执行你给定的预期字符串要求;它不能决定那个预期字符串是否表达了 LIST-3。
这些弱检查实际使用 Node.js v24.18.0 运行过,没有网络请求,也没有安装测试依赖:
| 实现 | W1 | W2 | W3 | W4 | 弱检查结果 |
|---|---|---|---|---|---|
| correct | 通过 | 通过 | 通过 | 通过 | 4/4 |
| firstOnly | 通过 | 通过 | 通过 | 通过 | 4/4 |
| sortedUnique | 通过 | 通过 | 通过 | 通过 | 4/4 |
全绿结果本身是准确的。但如果把它解释成“保留了顺序和重复项”的证据,这种解释就不准确。
从需求推导遗漏用例
运行更强的检查之前,先根据 LIST-3 写出字面预期字符串。表中的 \n 表示一个 LF 字符,并不是两个输出字符。
| 用例 | 完整输入 | 独立指定的预期输出 |
|---|---|---|
| C0 | [] |
"" |
| C1 | ["src/app.js"] |
"- src/app.js\n" |
| C2 | ["src/z.js", "src/a.js"] |
"- src/z.js\n- src/a.js\n" |
| C3 | ["src/a.js", "src/a.js"] |
"- src/a.js\n- src/a.js\n" |
| C4 | ["notes/Read Me.md", "界面/按钮.js"] |
"- notes/Read Me.md\n- 界面/按钮.js\n" |
| C5 | ["docs/z.md", "src/app.js", "docs/a.md"] |
"- docs/z.md\n- src/app.js\n- docs/a.md\n" |
C2 是这里最小而有用的挑战。它包含后续项目,而且初始顺序会被排序改变。C3 单独要求保留重复项;C4 检查带空格和非 ASCII 文本的标签能否原样保留。C5 则检查更长的序列。
完整数据文件 B84-02-cases.json 存放了这些字面预期。每个用例改进后的检查是:
for (const c of cases) {
assert.equal(format(c.input), c.expected);
}
不要通过调用被测实现来生成 c.expected,也不要在不核对规则的情况下,把实现的返回值直接复制进示例数据。这样会抹掉你原本试图揭示的、来自独立依据的差异。
对照预测与实际执行
预测结果是根据需求和三个函数写出的。运行用例后,得到同样的矩阵:
| 实现 | C0 | C1 | C2 | C3 | C4 | C5 | 实际结果 |
|---|---|---|---|---|---|---|---|
| correct | 通过 | 通过 | 通过 | 通过 | 通过 | 通过 | 6/6 |
| firstOnly | 通过 | 通过 | 失败 | 失败 | 失败 | 失败 | 2/6 |
| sortedUnique | 通过 | 通过 | 失败 | 失败 | 通过 | 失败 | 3/6 |
对 C2,firstOnly 实际返回 "- src/z.js\n";sortedUnique 返回 "- src/a.js\n- src/z.js\n"。它们分别与同一个预期字符串的不同部分矛盾。
本地运行器为每个实现单独启动进程。两个错误实现面对改进后的测试套件时,都以退出码 1 结束。它们的断言失败保留在配套运行日志中,没有为了让整包材料看起来全部通过而被移除。完整示例的执行结果记录了运行时、用例、退出码和日志文件名。这些执行检查的是所展示的函数与固定用例,不是客户项目,也不构成穷尽性的正确性结论。
同样质疑新增断言
假设智能体提出新增以下测试:
assert.equal(
format(['src/z.js', 'src/a.js']),
'- src/a.js\n- src/z.js\n'
);
它期望排序,而 LIST-3 禁止排序。在这个用例中,实际执行会拒绝 correct,却接受 sortedUnique。因此,新增失败本身不足以成为修改实现的理由:测试的预期值也需要需求依据。
如果 LIST-3 仍是双方约定的需求,就移除或修正那条断言。如果用户确实想要排序输出,就记录变更后的需求,并有意更新相关示例。不要为了让测试全绿,就悄悄改变规则。
复制这份已通过测试套件的审查请求
按 LIST-3 审查虚构的变更文件列表格式化函数,不要以实现
当前的输出为标准。使用 B84-02-cases.json,以及
format-implementations.mjs 中的三个完整函数。
先说明现有每条弱断言能确立哪项需求。解释为什么针对空结果的
every() 断言是空真,为什么单项输入不能展示后续项目是否保留,
以及为什么只检查桩的断言没有运行目标函数。
保留合理的检查,不要把它们全部删掉。
执行之前,根据 LIST-3 为 C0-C5 推导字面输出。
对 correct、firstOnly 和 sortedUnique 应用相同的精确输出断言。
返回实现与用例的矩阵、分歧处的实际输出、命令、运行时,
以及保留的原始失败。尚未运行的预测必须如实标注。
独立检查拟议的排序输出断言。它与当前要求保留顺序的需求矛盾。
不要因为某个实现目前会产生该结果,就编辑 LIST-3 或认可
那个预期值。
工作只限这些新的教学文件。不安装依赖、不修改项目测试套件、
不读取客户代码,也不进行生产执行。返回有明确范围的测试建议;
独立复核和接受仍是后续单独步骤。不要从两个错误替代实现和
六个具名用例中,推断穷尽覆盖或一般性的变异测试得分。
现在,一次有用的审查可以指出缺少的区别:“展示第二个项目仍处于原来的位置。”这是一项具体的下一步检查,预期答案来自需求,而不是智能体当前的实现。