基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
AIHT
内容
精选全部 AI 动态热点榜AI 日报收藏
分类
模型行业编程产品技术观点开源
更多
Agent 接入
返回首页开发者导航
AIHT
精选 / 资讯详情
9.0
重磅
AI SCORE
编程提效2026-10-31 20:04

AI写的测试全绿,如何验证需求真被满足

dev.to · AI#AI编程#变异测试#持续集成
Editor brief · 编辑速览

文章给出两项CI检查:将稳定的验收条件编号关联到实际执行通过的测试,再用变异测试检查测试能否发现错误。示例基于Node、Vitest和Stryker的真实运行,并提供可复制代码。

文章思维导图
Knowledge map
拖拽缩放
AI deep read

AI 深度解读

Agent 写完功能,再给自己补一套测试,CI 全绿就能交差了吗?作者用一个 CSV 导出案例说明:代码和测试可能一起偏离需求。测试通过,只能证明它们彼此认可,不能证明功能符合规格。

作者给出的补救办法是两道 CI 检查:先确认每条验收标准都有实际运行且通过的测试,再用变异测试检查这些测试能不能识别错误。案例跑通了,但它仍然替代不了对需求和关键断言的人工审查。

为什么测试全绿,需求却没实现

作者的实验环境是 Node 24、Vitest 5.0.5 和 Stryker 10.1.1,功能只有一个:把交易记录导出为 CSV。规格里写了三条带稳定编号的验收标准:

  • AC-1:不加筛选导出时,包含用户全部交易,按时间从新到旧排列。
  • AC-2:描述包含逗号或双引号时,按照 RFC 4180 给字段加引号并转义。
  • AC-3:筛选结果为空时,文件只包含表头。

作者故意写弱了实现和测试。实现只处理逗号,漏掉双引号;AC-1 的测试只数行数,不检查顺序;AC-3 则用了 it.skip。

下面这条测试最能说明问题:标题声称验证逗号和引号转义,实际只检查结果里有没有 Coffee。

it('AC-2: escapes commas and quotes', () => {
  const csv = buildCsv([row({ description: 'Coffee, "large"' })])
  expect(csv).toContain('Coffee')
})

要看的是最后一行:即使完全不做转义,toContain('Coffee') 仍然成立。

Vitest 给出的结果是 2 passed | 1 skipped (3)。如果再用 grep 搜索 AC 编号,三条标准也都能找到。测试标题写了什么、断言验证了什么、测试到底有没有执行,是三件不同的事。

测试名称可以声明覆盖需求,只有断言才能检验行为。

第一道检查:需求编号必须对应真实测试结果

作者没有继续扫描测试源码,而是读取测试运行器生成的 JUnit XML。Vitest 配置使用 reporters: ['default', 'junit'],并通过 outputFile: { junit: 'reports/junit.xml' } 保存报告。

检查脚本 scripts/check-spec.mjs 默认读取 SPEC.md 和 reports/junit.xml,也允许通过命令行参数替换路径。它用 /\bAC-\d+\b/g 提取需求编号,再从 <testcase> 的名称和结果中建立对应关系。

规则很具体:规格中的编号没有测试,报 no test;关联测试出现失败,报 failing;只有跳过的测试,报 only skipped tests。反过来,测试里有编号、规格里却没有,也会报错。

因此,执行 node scripts/check-spec.mjs 后,AC-3 被识别为只有跳过的测试。作者又把规格里的 AC-3 改成 AC-4,测试保持原样,脚本同时报告 AC-4 缺测试、AC-3 已不在规格中。这能发现需求改名后测试没有同步的问题。

如果使用 Spec Kit 的 FR-001 编号,只需要调整提取编号的正则。作者还实测过 Node 内置运行器的 node --test --test-reporter=junit,脚本无需修改。Jest、Playwright 虽然也能输出 JUnit,但作者没有验证,不能直接假定跳过状态的表达方式相同。

取消 .skip 后,结果变成 3 passed (3),脚本报告 3 criteria covered。可双引号依然没转义,排序也依然没人检查。

这道检查只回答「有没有对应且通过的测试」,不会理解断言是否充分。

第二道检查:故意改坏代码,测试能不能报警

变异测试(mutation testing)会对现有代码做小幅修改,比如翻转条件、删除方法调用或清空字符串,再运行测试。每个修改后的版本叫 mutant。如果测试仍然通过,这个 mutant 就存活了。

安装命令是 npm i -D @stryker-mutator/core @stryker-mutator/vitest-runner。作者配置 testRunner: "vitest"、mutate: ["src/csv.js"]、reporters: ["clear-text", "progress"] 和 coverageAnalysis: "perTest",然后执行 npx stryker run。

这个单文件案例运行了 3 秒,生成 21 个 mutants,其中 6 个存活,得分 71.43%。两个结果直接暴露了测试漏洞:

  • 把 if (text.includes(',')) 改成 if (false),彻底关闭转义,AC-2 仍然通过。
  • 把 [...rows].sort((a, b) => b.date.localeCompare(a.date)) 改成 [...rows],删除排序,AC-1 仍然通过。

测试标题里的编号此时很有用:报告可以把存活的变异直接关联到验收标准,方便审查者找到薄弱断言。

但 Stryker 没有直接指出双引号处理缺失。原因也很直接:它修改的是已经存在的代码,没有写出来的逻辑,未必能通过修改现有语句暴露。

作者随后让 AC-1 比较完整输出及日期顺序,把 AC-2 拆成逗号、双引号两条测试。后者要求输出包含 ,"The ""large"" one",,终于触发 1 failed | 3 passed (4),规格检查也报告 AC-2: failing。

修复字段转义的核心代码只有一行:

if (/[",]/.test(text)) return `"${text.replaceAll('"', '""')}"`

要看的是 replaceAll('"', '""'):除了给字段外层加引号,还要把内部双引号成对转义。修复后,两道检查通过,Stryker 杀死全部 23 个 mutants,得分 100%。

我的判断

我认可这套方法把「需求有测试」和「测试有约束力」分开检查。它比单纯要求 Agent 补测试数量、补覆盖率,更接近团队真正关心的问题:功能退化时,CI 能否拦住。

但原文开头的「证明」需要收窄理解。两道检查都通过,仍然不能证明实现完整符合规格。 一个断言很强、却标错 AC 编号的测试,可能通过全部检查。规格如果漏掉 100,000 行导出的要求,工具也不会自动补上。

变异分数同样不能直接充当质量结论。这个案例只有一个小函数;真实项目可能存在不改变行为的等价变异,无法靠合理测试杀死。作者没有提供大型代码库、长期运行成本或其他方案的对照数据,100% 是案例结果,80 的门槛则是作者为文章选择的起点。

实现上还有边界:检查脚本用正则提取 XML,作者验证了两种运行器的输出,原文没有证明它是通用 JUnit 解析器。迁移前应拿团队实际报告验证。

执行顺序也有讲究:先 npx vitest run 生成报告,再 node scripts/check-spec.mjs,最后运行较慢的 npx stryker run。要让变异分数影响构建结果,需要配置 "thresholds": { "break": 80 }。作者实测的是得分 95.65%、门槛 100 时退出码为 1,并没有验证 80 适合一般团队。

Claude Code 的 Stop hook 只是补充思路。作者给出 npx vitest run; node scripts/check-spec.mjs || exit 2,因为 hook 只在退出码 2 时阻塞,而检查脚本失败返回 1;但他没有在演示中运行这个 hook。这里的分号也不会自动把 Vitest 失败转换成阻塞,落地时需要单独验证失败路径。

能拿来做什么

  • 给前端或 Node 功能的验收标准加稳定编号,再让对应测试带上编号。先从 CSV 导出这类输入、输出明确的功能接入 JUnit 检查,确保跳过测试和需求编号漂移能让 CI 失败。
  • 对关键规则检查具体结果,例如排序后的完整顺序、转义后的完整字段。代码覆盖率可以帮助定位没执行的代码,变异测试则补充检查「执行了却没有有效断言」的部分。
  • 大项目先把 mutate 限定到规格中「Files and interfaces」涉及的文件,再使用 npx stryker run --incremental 保存并复用结果,根据实际成本和存活原因调整门槛。
  • 把存活变异交给 Agent 时,按作者建议开启新会话,明确禁止修改 src/ 和 SPEC.md,只允许加强测试;如果新测试暴露实现错误,就停下来报告违反了哪条验收标准,再由人确认修复方向。
Original source

本文由 AI 通读原文后重新编写,观点部分为站内判断,细节以原文为准,来源 dev.to · AI,原文版权归原作者所有。

阅读原文
上一篇
让AI重写旧代码前,先设行为等价门禁
下一篇
把Twist讨论接入代理记忆,检索团队决策

资讯速览

dev.to · AI · 2026-10-31 20:04

AI 评分 9.0 / 10

相关标签

#AI编程#变异测试#持续集成

继续阅读

  • 119 行 Node.js 拆解 MCP 服务最小实现
  • 让AI重写旧代码前,先设行为等价门禁
  • 把Twist讨论接入代理记忆,检索团队决策
  • LLM 测试的隐形账单:调用频次放大成本
  • Codex 读取被截断,仍继续审查与决策