循环驱动开发:行为覆盖率优于代码覆盖率

nfcampos 发布于 2026-06-11 阅读 98

文章探讨了在编码代理时代,测试驱动开发的瓶颈从编写代码转移到知道测试应该断言什么,即测试oracle问题。作者提出行为覆盖率比代码覆盖率更重要,并分析了不同反馈源的质量排序,从自我评审到权威oracle。介绍了两种技术:挖掘高质量开源测试套件和程序化查询权威oracle(如Excel、Postgres)。最后指出,人类开发者的创造性工作提升为选择oracle、定义属性、比较方式和完成标准。

图像

预言机问题

借助编码 Agent 进行 TDD 是可行的。红-绿-重构(https://simonwillison.net/guides/agentic-engineering-patterns/red-green-tdd/)——编写一个失败的测试,让它通过,然后清理——以 Agent 的速度执行确实很好。

但瓶颈也随之转移。实现代码的成本几乎为零。测试脚手架的成本也几乎为零。真正的成本在于知道测试应该断言什么。困难的问题不再是“编写测试”或“编写代码”,而是:正确的测试或答案是什么?这就是测试预言机问题(Howden 1978, Weyuker, Barr 等 2015),在 Agent 时代被重新表述。预言机需要为正在开发的系统和行为提供权威答案,同时必须是外部的(不是被测试的系统)、确定性的(相同查询 → 相同答案)且可查询的(能够按需回答任意新输入)。这就是循环驱动开发(LDD):有意识地选择反馈源,并让 Agent 围绕它进行循环。

行为覆盖 ≠ 代码覆盖

行覆盖、语句覆盖和分支覆盖衡量的都是同一件事:某行代码是否在某个测试下被执行?即使这些指标都达到 100%,系统的大部分行为可能仍未定义——没有测试能确定给定输入下应该发生什么。代码确实执行了某些操作,但测试套件中没有任何内容表明这是正确的操作。

行为覆盖则提出了一个不同的问题:对于领域内有意义的输入场景,预期的输出是否被固定下来,以便测试可以进行断言?对于一个兼容 Excel 的引擎,SUM("42", TRUE)、条件为空的 SUMIF、包含循环引用的公式、通过 0.0;-0.0 产生的负零,以及日期序列号 60(1900 年闰年幽灵)都是不同的行为。代码覆盖关心一次函数调用;行为覆盖则关心所有变体。

覆盖工具无法告诉你哪些行为缺失,因为它们不知道存在哪些行为。这是一个领域问题——而预言机问题就是:谁来回答它?

并非所有反馈都同等重要

Agent 通过围绕反馈进行循环来改进——@RLanceMartin 的框架(https://x.com/rlancemartin/status/2064397389189071163):与其直接提示和引导模型,不如设计循环,让它能够根据环境反馈自我修正。因此,循环的质量就是其反馈源的质量。圣杯是能够按需提供外部且可查询的反馈。两个维度可以区分所有源:

独立性:反馈是与工作同源,还是来自工作无法触及的地方?一个对自己的输出进行评分的 Agent,同时扮演着学生和考官的角色。

可查询性:你是否能针对新的、具体的输入按需获得答案?还是反馈只是一个固定的、你只能阅读的产物?

图像

四个象限:

自我评分且静态:重新阅读自己的计划/笔记

自我评分且可查询:线程内的自我审查

独立且静态:标准/RFC 文本;挖掘出的测试套件

独立且可查询:线程外审查;实时预言机

按从最差到最佳排序:

  1. 线程内审查:编写代码的上下文进行评分。可查询——你可以问它任何问题——但完全没有独立性:被要求评估自己工作的 Agent“倾向于充满信心地赞扬工作——即使质量明显平庸”(https://www.anthropic.com/engineering/harness-design-long-running-apps)。答案来自被评估的事物本身。

  2. 标准/RFC 文本:效果不如预期。完全独立——终于有了外部权威——但却是静态的。你不能向规范询问你的实现应该为某个特定畸变输入返回什么;你只能阅读文本并进行解释,而解释者就是被评分的同一个 Agent,因此依赖性通过阅读过程悄悄地回归。规范文本也存在欠定性:对边界情况保持沉默,并且与真实实现的行为存在偏差。我们在构建无头 Excel 引擎时亲身体会到这一点:OOXML(ECMA-376)是现存最全面的规范之一,而编码 Agent 对它的生产性利用却少得惊人——而真正的 Excel,作为预言机被查询,直接驱动了测试套件。如果目标是兼容一个真实系统,那么规范就是它的有损代理。

  3. 线程外审查(新线程、子 Agent、评估 Agent):独立上下文,同样类型——并且可查询,事实证明这比残余的依赖性更重要。Lance Martin 报告说,验证子 Agent 往往优于自我批评(https://x.com/rlancemartin/status/2064397389189071163),因为评分发生在独立的上下文窗口中;Anthropic 的框架设计工作(https://www.anthropic.com/engineering/harness-design-long-running-apps)将生成器与评估器分开,类似于 GAN 风格,评估器能够捕获生成器认为已完成但实际上只是 stub 按钮和仅显示功能的问题。这仍然是一种判断,而非测量——循环中没有基本事实,只有动机较弱的第二意见。在那些没有可编写脚本的权威(如设计质量、用户体验感觉)的领域中,这也是可用的最佳阶梯,其中新上下文的评判者近似于预言机,而非替代它。

  4. 高质量测试套件:独立且已预先查询:每个用例都是一个冻结的问题,带有可执行的答案,零解释步骤。但仍然是静态的——覆盖范围由其他人枚举固定;你无法询问他们没有写下来的情况。

  5. 可模仿的预言机:独立、确定且可查询:你可以构建的任何输入都能按需获得一个权威的、可执行的答案。这是其他四个近似达到的象限。

这个排序蕴含了两个教训。测量胜过判断:测试套件和预言机优于两种形式的审查,因为它们返回的是每个输入的、机器可检查的答案,而不是意见。文本权威效果不佳:规范文本听起来像是基本事实,但由于它无法回答每个输入的问题,在实践中,在循环中的作用甚至不如新上下文中的同类型审查者——解释将预言机的角色直接交回给被评分的 Agent。

两种技术

当 Agent 能力很高时,限制因素就是“X 做什么?”。有两个答案——反馈排名的前两个阶梯。

A. 挖掘一个高质量的开源测试套件

找到一个已经在你领域内列举了行为的项目。使用他们的测试作为你的规范。Agent 针对你的实现进行移植/调整/重新生成。这不是一个新想法(TCK、一致性测试、基于参考的实现特征测试)——改变的是,Agent 可以在几分钟内完成移植,进入那些人类根本懒得去做的细分领域。

一个有用套件的特征:

  1. 断言固定的是系统类别的行为,而非实现细节。一个断言 parse(input) == expected_ast 的测试固定了一个行为不变量。一个断言“使用大小为 100 的 LRU 缓存”或“以这种结构形状存储 token”的测试则固定了一个项目的选择,并且在重新实现时会失效。

  2. 契约在系统的外部边界上,而不是在一个项目选择的内部接缝处。真正的一致性测试——test262(“JavaScript 做什么”)、SQLLogicTest(“SQL 做什么”)——测试系统类别。虚假的一致性测试则验证一个项目选择的接口设计,仅当重新实现者恰好以相同方式分解事物时才有用。类别级别的行为通常存在于项目的主测试套件中,即使这些测试表面上看是“内部的”(宿主语言耦合、私有导入)。多语言 Agent 将其作为移植成本吸收。阅读断言的内容;询问契约是在系统的外部边界上,还是在一个项目的内部接缝处。

  3. 源语言几乎无关紧要。Python 测试可以催生一个 Rust 的持久执行引擎;JS 测试可以催生一个 C# 的数字格式化移植;CommonMark 规范的 JSON 用例(https://github.com/commonmark/commonmark-spec)已经催生了数十种语言的 Markdown 实现。

具体来说:一个 JS 数字格式化库的测试(https://github.com/borgar/numfmt/tree/master/test)可以作为参数化用例在任何目标语言中重新生成,并且上游的提交随后可以作为检测到的失败流入。一个语法解析器可以通过将真实世界的输入语料同时提供给实现和参考解析器,并比较输出结果来验证——不需要编写预期输出,因为参考实现会提供它们。纯差分方法。

B. 通过编程方式查询一个权威预言机

回到是什么让一个预言机成为预言机——它是外部的(不是被测试的系统)、确定性的(相同查询 → 相同答案)且可查询的(它按需回答任意新输入)。外部排除了 Agent 既是用例作者又是权威的自我检查循环。确定性排除了像 LLM 和未接地生成器这样的非确定性参考。可查询排除了静态参考——规范文本、固定用例列表——它们无法回答未预料到的每个输入问题。任何不能满足这三个条件的东西,对于 LDD 的目的而言都不是预言机——它是一种不同类型的产物和一个不同的问题。

编写脚本连接实时参考。捕获输入,询问预言机,将答案冻结在仓库中,在 CI 中针对冻结的答案进行断言。

一个有用预言机的特征:

  1. 权威:对于这个领域,它的答案就是最终答案。构建一个兼容 Excel 的引擎?用真实的 Excel。构建一个兼容 Postgres 的解析器?用真实的 Postgres。DuckDB 使用 SQLLogicTest 针对参考引擎测试其 SQL 实现(https://duckdb.org/docs/stable/dev/sqllogictest/intro)——在流行数据库中的相同模式。

  2. 可脚本化且全表面:Agent 可以驱动参考实现的任何可观察行为,而不仅仅是经过策划的子集。典型的全表面示例:macOS 上的 AppleScript,Windows 上的 COM/OLE Automation,用于 HTTP 服务的 curl,暴露完整对象模型的语言绑定(Excel 的 xlwings,Postgres 的 libpq)。像 MCP 服务器或单一用途 REST API 这样的窄网关通常过于受限——你只能问别人认为应该暴露的内容。

  3. 缓慢且昂贵也没关系:每次查询只运行一次;结果在仓库中冻结。CI 不会承担这个成本;只有刷新运行才会。

操作产物是一个可重新运行的脚本,与快照一起检入。脚本执行实时预言机查询;快照捕获结果;CI 针对快照进行断言。当你需要不同的输出、怀疑出现漂移或行为发生变化时——修改脚本并重新运行。快照会重新生成。脚本是来源证明,脚本是版本锁定,快照是捕获的输出。不需要其他东西。

距离断言可以替代相等断言。不必通过相等(SUM("42", TRUE) == 43)来固定每个行为,而是断言整个输出上的距离(pixel_diff(impl, oracle) < 1%;Playwright 的 toHaveScreenshot({ maxDiffPixelRatio }) 是一个主流实例:https://playwright.dev/docs/test-snapshots;浮点数的相对误差和字符串的编辑距离也是同样的原理)。距离断言更容易设置——一个断言覆盖整个输出,无需事先列举哪些细节重要——并且它在无法使用干净的相等断言时也能工作(抗锯齿、浮点数累积、自然语言输出)。代价是可行性:一个失败的差异不会告诉你哪个细节出错或下一步该修复什么,领域不变量仍然隐式存在。无论哪种方式,Agent 都会得到一个聚合的爬山信号:相等套件中通过的百分比,或跨迭代推动减少的像素差异百分比。

一个典型的设置:快照生成器接受一个用例列表,通过编程方式调用预言机,以每个用例的指纹为键捕获输出,并写入 JSON。测试运行器从不调用预言机——CI 通道和开发者/刷新通道通过快照文件解耦。生成器支持针对性的刷新(--case)和全面重建(--refresh-all)。当行为问题出现时,修改脚本的用例定义并重新运行。(对于一个兼容 Excel 的计算引擎,这看起来像:跨数学/三角、统计、查找、文本、lambda、日期类别的公式用例;xlwings 调用 Excel;结果按每个用例的指纹捕获到 JSON 中。)

Agent 时代真正的新东西

当测试的生产者和消费者都是编码 Agent 时,有三件事真正发生了变化:

  1. 成本效益倒挂:对于人类来说,为 50 个边界情况查询一个真实参考是一个繁琐的半天工作,所以人类通常会跳过;套件保持贫弱;bug 在生产中浮现。对于 Agent 来说,50 次预言机查询只需要 90 秒。边际预言机成本几乎为零。这是真正的解锁——不是相同循环的更快版本,而是项目最终收敛到的由预言机派生测试数量的均衡点发生了根本性转变。

  2. 预言机、测试和实现在一个工作流中:在 Agent 之前,工作流有多个阶段:咨询预言机,内化答案,然后根据你的心智模型编写测试,然后编写实现。每次交接都会损失保真度——测试最终反映的是开发者的记忆,而非预言机本身。有了 Agent,所有这些都浓缩到一个会话中:查询,将答案冻结为测试,编写实现。测试就是预言机的答案,而不是回忆。

  3. 挖掘变得多语言:跨语言测试挖掘以前需要人类同时熟悉源和目标技术栈,所以几乎从未发生过。Agent 可以轻松做到:测试不再“在你的技术栈中”,而是开始成为恰好用某种技术栈编写的行为规范。

人类创造力转移到的位置

LDD 并不会移除人类开发者;它将工作提升了一个层次。创造性的行为不再是手动推导 SUM("42", TRUE) 或将答案复制到断言中。它在于做出四个决策:

  1. 选择哪个预言机:通过 xlwings 调用真实的 Excel?用于正则表达式移植的 CPython 的 re 模块?用于 SQL 解析器的 Postgres?这个选择既固定了你要引用的权威,也固定了你可以查询的表面。

  2. 固定哪些属性:预言机产生大量可观察的输出;你声明哪些算数。公式结果值:算。结果类型(数字 vs 错误 vs 空白):算。15 位浮点精度:算,每个用例有容差。墙上时间、计算链顺序、内部表示:不是预言机应该指定的,即使 Excel“有答案”。固定一切会导致套件脆弱;固定太少会导致大多数行为未定义。

  3. 采用什么形式的比较:基于规范化(类型,值)元组的每单元格相等断言。阈值下的像素距离。有限编辑距离。这个选择决定了失败的样子——一个不匹配的单元格,还是一个 1.7% 的像素差异而没有指向原因的指针。

  4. 何时 Agent 算完成:不是“所有测试通过”——空套件也能轻易通过。完成是指:你命名的每个行为维度都有用例,用例列表覆盖了该功能的输入空间,预言机脚本已检入,快照可从脚本重现,CI 无需实时预言机即可断言,并且新行为必须要么匹配预言机,要么故意更新它。如果不将这些写在一个 Agent 每次回合都会重新检查的地方,循环编码 Agent 要么过度行事,要么过早停止——Codex 的 /goal 继续模板(https://github.com/openai/codex/blob/6014b6679ffbd92eeddffa3ad7b4402be6a7fefe/codex-rs/core/templates/goals/continuation.md)是一个具体的实现;Claude Code 中的 /goal 和 Claude Managed Agents 中的 Outcomes(https://x.com/rlancemartin/status/2064397389189071163)是其他实现——Outcomes 会生成一个独立的评分子 Agent,在 Agent 可以停止之前,必须确认规则已满足,从而将完成检查保持在线程之外。

这仍然是创造性的工程——只是对训练环境的创造力,而不是对每一行实现的创造力。

先前工作

每个组成部分都早于 2026 年。

特征测试(Feathers 2004):将现有系统用作自己的预言机。LDD = 相同的机制,被特征化的系统是外部的且权威的。

审批/快照测试(Falco c. 2008, Verify, Jest):相同的持久化模型。唯一的区别:LDD 的真相来源是外生的;快照测试的真相来源是内生的(“代码上次做了什么”)。

一致性测试套件(test262, SQLLogicTest, WPT, TCK):最接近的组织类比。SQLLogicTest 的完成模式(针对参考引擎运行原型以填充预期结果)本质上就是大规模的 LDD,早在 2003 年。LDD 的贡献是使相同的模式在任何细分领域中都变得随意且存在于仓库中。

差分测试(McKeeman 1998, Csmith 2011):技术 B 就是差分测试,其中差异在规划时发生并被冻结,而不是在 CI 中实时运行。

测试预言机问题(Barr 等人 2015):LDD 在他们分类中是“派生预言机”。继承了这个框架。

各个部分都是旧的,循环是廉价的;剩下的工程任务是反馈——一旦你以这种方式看待代码,就很难不问:编写软件是否真的和训练模型那么不同。

  • 原文链接: x.com/nfcampos/status/20...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~

相关文章

0 条评论