1585 字
8 分钟
禁止读实现代码,才能发现代码里根本没写的分支

边界用例由谁设计,决定了它能发现什么。读过实现的人写出的用例只能验证已有分支,实现漏掉的分支,用例也会跟着漏掉。我的判断是:黑盒的价值不在于模拟用户视角,而在于设计者确实不知道实现是怎么写的。

白盒与黑盒双轨,以及三类比对结果

读过实现之后,用例只能复述实现#

AI 写代码真正容易出问题的地方不是把逻辑写错,而是对需求的理解本身存在缺口。代码忠实实现了那份不完整的理解,内部完全自洽:没有矛盾、没有死代码、测试全部通过。读代码发现不了这类缺口,因为代码里没有留下对应的痕迹。

从实现出发设计的用例,上限就是实现已经做到的事。它能发现条件写反、类型不匹配、异常类型收窄,但「这里本该有一个分支」不会出现在任何一条用例里——用例照着分支列表写,列表上不存在的条目不会被补上。

所以我给这类子代理的第一句话是禁令,不是任务描述:你拿不到、也不要去读被测代码的实现,这是有意安排的;你的价值恰恰在于不受实现影响。这句话在 prompt 里的英文写法更直接:your value comes precisely from not knowing how it is implemented。

禁令要写到可执行的粒度。「做黑盒测试」是一句口号,「不要打开实现目录下的任何文件、不要搜索源码、不要运行 diff 命令」才是约束。边界要划准,不是全盘封锁:README 和设计文档允许读,用于了解项目背景;实现目录一概禁止。

禁令必须配一份补偿#

切断一个信息源,就要补上等价的信息,否则子代理只能自行猜测。补偿是一份手写契约,不能是接口文档的复制。

契约按分支状态组织,不按字段组织。每条规则把开关开启、开关关闭、依赖缺失三种状态分别写明预期;三态写全了,子代理才知道哪些组合各需要一条用例。

业务不变量用绝对化的句式锁定,并给出取舍方向。取计费用量的那条我写的是:绝不允许把上一轮的用量当作本轮返回,任何无法确定归属的情况必须返回空——宁可少记不可重记。子代理遇到模糊边界时就知道该优先保证哪一侧,不需要停下来提问。

失败语义要写成可判定的:异常必须原样向上抛出,不得在中间层捕获后忽略,且恰好记录一条日志。合法但反直觉的用法也要写进去,例如「传入过去的时刻是合法输入,语义为立即回收,不应被上限逻辑拦截」——不写明,子代理会把它当成非法输入。契约里还固定附一组跨任务复用的边界输入:空值、空串、纯空格、大小写变体、前后带空格、超长字符串、路径穿越、注入片段、emoji。

质量门只有一条:每条用例的期望行为必须具体。「应该报错」不合格,要写成「拒绝并返回指定错误码,且不产生任何副作用」。

这条禁令还有一个我没有预期到的副产品。因为不许读代码,契约必须在 prompt 里写全:默认值与上下限、完整错误码与状态码、状态机的全部取值、同键并发时谁创建谁等待、运行环境里有哪些命令可用。写完之后这段 prompt 本身就是一份规格说明书,而且它不会过期——写不全,子代理就推不出用例。

差集才是产出#

黑盒轨单独使用时产出有限。它只知道契约声明了什么,不知道实现额外做了什么,会给出大量早已覆盖的用例。

所以同一批改动并行执行两条轨:白盒轨读代码,从真实调用路径、异常分支和并发点反推用例;黑盒轨只读契约,推导「理应接受 / 理应拒绝 / 必须安全降级」的清单。两条轨互相看不见,各自产出一份表,最后比对。

比对结果分三类:

  • 两边都有:正常覆盖,按优先级排期。
  • 黑盒有、白盒没有:契约要求了而代码没有做,属于漏实现的高危项,必须逐条确认。差集里最有价值的是这一类。
  • 白盒有、黑盒没有:代码做了契约没要求的事。要么是契约漏写(补契约),要么是多余的防御分支(删除或注明理由)。

比对的是语义而非字面,所以这一步由人来做:同一件事可能一边写「空字符串」、另一边写「长度为零的输入」,按字面自动比对会把它判成差集。两份清单各几十条,人工比对的成本可控。这两条轨是分阶段验证流水线里边界测试阶段的具体做法。

当前选择:契约变更才执行双轨#

涉及对外接口、数据模型或并发语义时我执行完整双轨;只改函数内部实现、文案或日志时只执行白盒轨。判据是契约有没有变——契约没变,黑盒轨会输出与上次相同的清单。

黑盒轨还有一个前置用法:写代码之前先执行一次,拿到的清单就是实现待办。同一份清单,写完代码生成是检查表,写之前生成是施工图纸,成本相同。

代价是手写契约无法省略,一个中等复杂度的接口要写几百字,写得含糊,子代理就会产出含糊的用例。如果一个接口的契约我自己都写不清楚,那说明应该先去澄清需求,而不是先去设计测试。