Agent 主导开发的测试概念:从验收规格到反例、状态与评估

This article is extracted from the chat log with AI. Please identify it with caution.

AI 参与说明(Agent:Codex):本文由 Codex 基于文中所列研究论文、测试工具官方文档与工程实践资料协助整理。概念分类和落地顺序是工程建议,不是新的测试标准。资料整理于 2026-09-07。

本次撰写与校验运行记录:模型 gpt-6-astra;reasoning effort:medium;执行入口:Codex Desktop;提供方:openai;CLI 版本:0.153.4(不代表桌面 App 版本)。参数来自本次任务运行记录。

核心观点:从“多写测试”转向“获得什么证据”#

Agent 可以同时生成实现、测试和 Mock。如果它们共享同一个错误假设,测试可能全部通过,却不能满足业务要求。因而需要分别检查三个问题:预期结果依据什么制定,测试有没有机会发现实现错误,以及失败能否稳定复现。

这些需求大多对应已有的测试思想。Agent 开发改变的是使用它们的成本与必要性,并没有让 Unit Test、Integration Test 或 E2E Test 失去价值。也不必把每个功能都交给 LLM Judge:明确的数值、状态、权限与副作用,通常可以直接断言。

概念地图:这些名称不在同一个维度#

维度代表概念要回答的问题
验证对象与范围Unit、Component、Integration、Contract、E2E测的是哪一段真实系统,哪些依赖被替换?
规格与工作方式Acceptance Criteria、Executable Specification、TDD、BDD预期行为从哪里来,怎样约束实现?
输入与反例生成Example-based、Property-based、Metamorphic、Model-based、Fuzz Testing除了想到的几个例子,还能怎样寻找错误?
对照与判定依据Test Oracle、Reference Model、Differential、Characterization、Snapshot / Approval根据什么说结果对、错或发生了变化?
测试质量与运行环境Mutation Testing、Coverage、Hermetic Testing、Deterministic Replay测试是否敏感、隔离、可复现?
非功能属性与约束Performance、Security、Accessibility、Architecture Tests功能之外的成本、权限、体验和结构是否符合要求?
Agent 自身Task Evals、Trajectory Checks、Outcome ChecksAgent 是否完成任务、遵守约束,以及结果是否可靠?

例如,同一个测试可以在 Unit 范围运行,采用 Property-based 生成输入,用参考模型判定结果,再用 Mutation Testing 检查断言是否足够敏感。这些术语可以组合,不是一条只能选一个位置的测试层级。

Test Oracle:谁定义“正确”#

Test Oracle 是判断观察结果是否符合预期的依据或机制。它可以是明确的期望值、业务不变量、独立参考实现、已审阅的基线,也可以是人工或模型评分。

假设需求是“未登录时禁止提交”,Agent 却理解成“离线时一律入队”。若测试也从后者推导期望值,实现和测试就会一起出错。独立性首先来自规格、证据和变更控制,而不是简单地换另一个 Agent 写测试。

实践中应保留可追踪的需求 ID,把关键期望值连接到业务规则。编码 Agent 可以提出规格变更,但不能通过删除断言、修改基线或跳过失败场景来自动宣告修复。模型评分则要记录评分规则、模型配置、实际输入证据,并用人工标注的正反例校准。

Acceptance Testing、Executable Specification 与 TDD#

Acceptance Testing 从可观察的业务结果验收功能,例如“同一个草稿重复提交,不产生两条业务记录”。Executable Specification 把规格连接到可以运行的检查;BDD 强调围绕行为与例子建立共同理解,不能仅等同于一种 Given / When / Then 文件格式。

TDD 是开发工作方式:先写一个体现目标行为的失败测试,再实现让它通过,最后重构并保留测试。它不规定必须使用哪种测试范围。让 Agent 执行这套循环时,要先确认测试因为预期行为尚未实现而失败,而不是因为导入错误或环境损坏而失败。Test Driven Development

它们适合减少需求歧义,但不能保证规格本身正确。对于小范围、低风险修改,不必机械地增加测试;应优先覆盖会造成真实损失的错误模式。

Property-based Testing:验证规则,而不只枚举例子#

Example-based Testing 指定几个输入与结果;Property-based Testing 定义输入空间和应成立的性质,由工具生成多组输入,寻找反例。部分工具还能通过 Shrinking 缩小失败输入,便于诊断。Hypothesis 官方介绍

对跨端业务逻辑,可以检查:

  • 在约定的非负金额域中,最终价格不应为负。
  • 对同一个业务操作 ID 的重复请求,不应重复产生业务记录。
  • 排序后的元素多重集合与原输入相同,且顺序符合比较规则。

性质必须说明适用域。例如,“增加商品不会降低总价”在有满减优惠时未必成立;“编码再解码得到原值”需要排除不支持的数据类型,并定义相等语义。错误的性质会把正确实现判为失败。

对 Agent 的任务可以从“多补几个测试”改成“列出关键不变量、适用范围和反例生成策略”。测试通过仍只是已执行输入的证据,不是对所有输入的形式化证明。

Metamorphic Testing:没有现成答案,也可以检查结果之间的关系#

有时很难为每个输入手算正确结果,但可以预测改变输入后,输出应满足什么关系。Metamorphic Testing 用这种关系构造后续测试。Metamorphic Testing 原始技术报告

例如,在明确不考虑顺序相关优惠、舍入和溢出的整数加总模型中,调换项目顺序不应改变总和。对于只做确定性排序的函数,重新排列输入后,排序结果应一致。

这类关系也可以由 Property-based Test 生成输入来检查,两者并不互斥。关系仍需经过审阅:搜索排序可能受个性化影响,图像变换可能改变识别目标,不能直接套用“输入变化、结果不变”。

Mutation Testing:故意改坏实现,看测试会不会失败#

Code Coverage 表示哪些代码被执行过,不能单独说明结果被认真断言。Mutation Testing 对程序施加小变化,例如把 > 改成 >=、删除分支或替换返回值,再运行测试。如果测试失败,就说明它识别了这次变化。Stryker:What is mutation testing?

对 Agent 生成的权限、边界比较和错误处理测试,这是一种有价值的质量检查。它直接追问:如果实现犯一个常见错误,测试能否阻止它?

存活的变异不一定都代表漏测:有些与原程序在适用域内等价,有些无法编译,有些超时。应区分状态并报告选择范围,不能把一个高 Mutation Score 当成系统正确率。优先检查关键模块或当前变更,控制计算成本。Mutant states and metrics

Model-based / Stateful Testing:验证操作顺序#

许多 Bug 不在单个函数调用中,而在“先做什么、再做什么”。Model-based Testing 用简化模型描述允许的状态与动作;Stateful Testing 保留动作之间的状态联系。工具可以生成动作序列,并把真实实现与模型比较。Hypothesis Stateful tests

例如,离线草稿可能经历:编辑、提交、超时、重试、退出账号、重新启动。每一步都应满足账号隔离、状态合法性与记录唯一性等要求。只验证“点击一次提交成功”,无法覆盖这些组合。

模型中的 Model 通常指业务状态模型,不是 LLM。它应比实现简单,避免复制实现的同一错误。普通顺序模型测试也不自动证明并发正确;并发操作还需要调度控制、历史记录以及明确的一致性判据。

Differential、Characterization 与 Approval:把现有系统作为对照#

这几种方法适合 Agent 重构、迁移或跨端重写,但其证据含义不同:

方法对照方式关键限制
Differential Testing同一输入交给 iOS / Android、新旧实现或多个独立实现一致可能是一起错;不同也可能是允许的平台差异
Characterization Testing先记录既有实现的实际行为,作为重构保护记录的是“现在怎样”,不能自动升级为“业务应当怎样”
Snapshot / Approval Testing输出与已审阅的快照或基线比较基线质量决定意义;自动接受所有变化会削弱保护

应明确哪些历史行为必须保持,哪些是准备修复的缺陷,并把后者改成符合规格的期望。对输出做归一化时,只排除真正无关的随机 ID 或格式差异,不能顺手忽略业务时间、权限字段或错误码。

既有系统难以直接测试时,可以先寻找可替换依赖、可观察结果的边界,再建立保护性测试。Legacy Seam

Fuzz Testing 与 Fault Injection:分别扰动输入和环境#

Fuzz Testing 大量生成或变异输入,寻找崩溃、挂起、断言失败或其他异常。它适合解析器、文件导入、协议和不可信输入边界;不是只能做纯随机字符串测试,也不能只靠“没有崩溃”判断业务正确。OSS-Fuzz

Fault Injection 则改变依赖或执行环境:超时、断连、失败响应、重复消息,或在明确时点中断进程。对同步、上传和重试功能,可以在测试环境注入“服务端已经成功,客户端丢失响应”,再验证恢复与幂等行为。

Chaos Engineering 通常进一步关注系统在受控扰动下的韧性假设、稳态指标和影响范围;并非每个超时 Mock 都需要称为混沌工程。日常 Agent 开发优先在隔离环境做可复现故障注入,按风险决定是否需要更大范围实验。Principles of Chaos Engineering

Hermetic Testing 与 Deterministic Replay:给 Agent 稳定的反馈#

Hermetic Testing 强调隔离或控制外部依赖;Deterministic Replay 强调记录并重放影响执行的输入、事件和调度条件。两者都服务于稳定诊断,但并非同义词。

应显式控制时间、随机数、数据初始状态、外部响应和资源清理。遇到不稳定测试,先定位原因;让 Agent 不断重跑直到一次成功,只会隐藏问题。重试可以帮助识别波动,但不能代替修复。Eradicating Non-Determinism in Tests

固定 Random Seed 也不保证一切可复现:网络、线程调度、文件系统状态和外部服务变化可能仍然影响结果。对生成式测试,应保存失败输入或事件序列,以及相关环境版本。

Architecture Tests 与非功能测试#

Architecture Tests 把允许的模块依赖和结构规则变成可检查的约束,例如“业务层不依赖 UI 框架”“客户端模块不导入服务端实现”。它们适合限制 Agent 在快速补功能时引入的依赖混乱,但不能判断架构本身是否适合业务。ArchUnit User Guide

功能通过也不代表性能、权限、可访问性或资源成本合格。按实际风险增加明确检查:越权访问是否被真实服务端拒绝,大文件处理是否超过资源预算,关键任务能否用辅助技术完成。阈值需要说明环境、样本和测量方式,不能用一次耗时结果宣称性能稳定。

Agent Evals:测试的是开发者代理本身#

当目标是比较模型、Prompt、工具配置或 Harness 时,被测对象是 Agent 的工作过程与交付结果。一个任务的最终测试通过,不等于代理在一批任务上可靠。

可同时观察任务完成率、代码或环境的实际最终状态、工具轨迹中的约束违规、成本和耗时。不要要求所有成功运行必须走完全相同的工具序列;只约束有业务意义的顺序或禁止行为。评估需要多次 Trial 与清楚的统计口径,不能把“多试几次至少成功一次”当作每次运行的成功率。Demystifying evals for AI agents

开发反馈用例与独立评估任务应适当分离,避免反复针对同一套题调优后高估泛化能力。评估标准和隐藏用例的修改权限也应独立管理。确定性判定、模型评分与人工检查可以组合,但每种证据的范围应分别报告。

最小可运行例子:性质与变异如何互补#

下面用 Python 3.9+ 标准库演示一个排序规格:输出既要有序,也要保留输入元素的出现次数。将代码保存为 test_sort_properties.py,运行 python3 test_sort_properties.py。输入是整数,示例不涉及自定义比较器、浮点 NaN 或对象稳定排序。

这不是完整的 Property-based 工具或自动变异框架:它显式枚举有限输入,并手动注入一个错误,用来说明两种思路的关系。

from collections import Counter
from itertools import product


def ordered(values):
    return all(a <= b for a, b in zip(values, values[1:]))


def verify(sort_fn):
    count = 0
    for size in range(5):
        for values in product((-1, 0, 1), repeat=size):
            result = sort_fn(values)
            if not ordered(result):
                raise AssertionError(f"Not ordered: {values} -> {result}")
            if Counter(result) != Counter(values):
                raise AssertionError(f"Elements changed: {values} -> {result}")
            count += 1
    return count


def wrong_sort(values):
    return sorted(set(values))  # Deliberate mutation: loses duplicates.


if __name__ == "__main__":
    print(f"Correct implementation: {verify(sorted)} cases passed")
    try:
        verify(wrong_sort)
    except AssertionError as error:
        print(f"Mutation detected: {error}")
    else:
        raise AssertionError("Mutation survived")

预期输出:

Correct implementation: 121 cases passed
Mutation detected: Elements changed: (-1, -1) -> [-1]

只检查“输出有序”,错误实现也会通过;补充“元素及次数保持不变”,才会抓住这一变异。这个例子说明测试性质需要足够完整,而不是证明排序函数对所有输入都正确。

如何用于日常 Agent 任务#

不必每次启用所有方法。先根据失败风险选择证据,再决定工具和成本:

改动优先方法希望获得的证据
修复明确 BugRegression Test,必要时按 TDD 推进未修复版本因目标缺陷失败,修复后通过
iOS / Android 重写同一业务功能共享规格、Property-based、Differential各端分别符合规格;允许差异有明确说明
离线同步与重试Stateful / Model-based、Fault Injection错误顺序与恢复场景下状态和副作用仍正确
大规模重构Characterization、Architecture、关键 E2E约定保留的行为不变,依赖边界与关键路径成立
解析或导入不可信文件Fuzz、资源限制与明确错误断言异常输入被处理,失败可重现
怀疑 Agent 测试过弱定向 Mutation Testing常见错误能被现有测试识别
更换编码模型或工具链独立 Task Evals、多次运行按统一任务和口径比较结果、约束与成本

对所有场景,共同要求是:说明替换了哪些依赖,保留实际执行证据,检查测试是否真的被发现和运行,并报告尚未覆盖的风险。自动生成的测试数量与 Coverage 可以辅助观察,不能代替这些判断。

关联阅读#

本文共 5222 字,创建于 Sep 7, 2026

相关标签: Tools, ByAI