分层测试优先提示:首次运行即获得正确代码
Source: Dev.to
如果你使用 AI 来帮助编码,最常见的失败模式并不是模型懒惰——而是目标模糊。
你请求一个修复,助手猜测 “正确” 是什么意思,结果得到看似合理但稍有偏差的代码:错误的边缘情况、错误的文件、错误的抽象、错误的依赖,或者正确的思路实现得过于宽泛。
降低这种失败率的一个简单方法是不要先请求修复。
先请求 测试。
为什么这样有效
大多数代码提示问题实际上是规格问题。
当你说:
Fix the currency formatter for German locales.
时,会有十几个隐藏的问题:
- 哪个文件拥有该行为?
- 具体期待什么格式?
- 使用的是哪个运行时或测试框架?
- 是否允许使用外部库?
- 解决方案应该是最小化的还是架构性的?
- 什么算作“完成”?
测试以一种通常的文字描述不会提供的方式回答这些问题。它为助手提供了一个可观察的目标,而不是模糊的意图。
这带来了三个直接好处:
- 正确性变得可执行。 你可以运行结果,而不是争论它。
- 范围保持更小。 当一个断言就能解决时,助手不太可能重构整个仓库的一半。
- 审查更容易。 变更是由失败的案例所证明,而不是手写的解释。
三步工作流
1. 提供最小可运行的上下文
只给出足够的信息,让助手能够定位工作内容。
包括:
- 所涉及的文件或模块
- 使用的语言/运行时
- 测试框架
- 一个具体的输入/输出示例
- 任何相关约束,例如 “不添加新依赖”
示例
I have formatCurrency(amount, locale) in src/format.js.
Runtime: Node 22.
Tests use Jest.
Expected behavior: formatCurrency(12.5, 'de-DE') should return '12,50 €'.
No new dependencies.
这就足够了。无需提供三段式的背景说明。
2. 仅请求一个失败的测试
这是关键一步。不要立即请求实现代码。 要求生成一个聚焦的测试文件,使用现有项目约定,只检查你关心的行为。
示例提示
Create a Jest test for src/format.js that asserts
formatCurrency(12.5, 'de-DE') returns '12,50 €'.
Only return the test file contents and any minimal config change required.
Do not implement the function yet.
为什么要分步?
因为这会迫使助手在动手编写代码前先思考预期行为。这本身就能大幅减少偏差。它还能让你立刻得到一个可运行的产物。如果测试有错误、含糊或不符合你的约定,你可以在任何实现之前就发现并纠正。
3. 请求最小的修复让测试通过
运行测试。如果失败,粘贴错误输出并请求最小的代码改动来满足该案例。
示例提示
This test fails with the following output:
[paste stack trace]
Implement the smallest change in src/format.js that makes this test pass.
Avoid unrelated refactors.
Return a unified diff.
这种表述很重要。 “最小改动” 与 “避免无关的重构” 并非装饰语——它们引导助手远离大幅重写,专注于可审查的补丁。
一个具体的例子
问题: CSV 导出错误——包含换行符的字段在电子表格应用中打开时会被拆分。
一个模糊的提示会是:
修复 CSV 导出转义。
这几乎必然会得到一个模糊的答案。
相反,下面是一个有结构的版本。
第 1 步 – 上下文
File: src/export/csv.ts
Runtime: Node 22
Tests: Vitest
Bug: values containing newlines are split into multiple rows in spreadsheet apps
Constraint: preserve the newline; do not replace it with spaces
第 2 步 – 先写测试
import { describe, it, expect } from 'vitest';
import { toCsvRow } from '../../src/export/csv';
describe('toCsvRow', () => {
it('quotes fields containing newlines', () => {
const row = toCsvRow(['hello\nworld', 'ok']);
expect(row).toBe('"hello\nworld",ok');
});
});
第 3 步 – 最小实现请求
现在助手有了一个明确的目标:
- 保留换行符
- 为该字段加引号
- 其他字段保持不变
这比“修复 CSV 导出”要紧凑得多。你更有可能得到一个正确的、本地的补丁,而不是一次投机性的重写。
为什么这通常比 “写代码” 提示更有效
直接实现的提示会让模型直接跳到解决方案的形态。有时这样可行,但往往助理会过早提交。
先写测试的提示会延迟这种提交。它让助理在选择结构之前,先通过可见行为来定义成功。这通常会在以下几个方面提升结果:
- 更少的虚构抽象
- 更少不必要的依赖
- 更好地保留已有约定
- 更容易调试,因为失败的断言会缩小搜索空间
它还能帮助 你 思考得更清晰。编写或审查测试会迫使你明确需求。你可能会发现预期输出有误、边界情况描述不完整,或者实际问题与最初想象的稍有不同。
该模式的优秀提示
以下是您可以改编的几个模板提示:
| 目标 | 提示 |
|---|---|
| 提供上下文 | “我在 src/date.js 中有一个函数 parseDate(str)。运行时环境:Node 20。测试使用 Mocha。期望:parseDate('2023‑01‑01') 返回一个表示 UTC 午夜的 Date。不添加新依赖。” |
| 请求一个失败的测试 | “编写一个 Mocha 测试,断言 parseDate('2023‑01‑01') 返回的 Date 满足 getTime() === 1672531200000。仅返回测试文件内容。不要修改实现。” |
| 请求最小修复 | “上述测试失败,错误信息为:expected 1672531200000 but got 0。提供对 src/date.js 的最小改动,使测试通过。返回统一差异(unified diff),并避免无关的更改。” |
您可以自由组合这些部分,以适应您的语言、运行时和测试框架。关键始终是:上下文 → 失败的测试 → 最小修复。
一致的指导
- “首先编写一个聚焦的失败测试。”
- “使用现有的项目约定。”
- “暂时不要实现。”
- “仅返回测试文件内容。”
- “实现最小的改动以使该测试通过。”
- “避免不相关的重构。”
- “返回统一的 diff。”
这些小约束综合起来,使工作流更加可靠。
常见错误
一次请求太多
One bug, one test, one patch.
如果在单个提示中请求三个边缘情况、一次重构以及更新文档,你会失去主要优势:紧密的反馈。
给出无环境的提示
仅说“写一个测试”,而不指明 Jest、Vitest、pytest 或文件结构,会导致得到的想法在错误的格式中。
接受未运行的测试
生成的测试仍然是代码——请运行它。一个损坏的测试文件不是合同;它只是一段格式更好但仍是幻觉的代码。
让实现无限增长
测试一旦存在,就要坚持原则。请求最小的通过更改,而不是“最干净的长期重构”。在确保正确性后,你随时可以进行重构。
何时不使用它
当行为已知且正确性至关重要时,这种模式表现出色。它在以下情况下用处较小:
- 探索性原型开发
- 开放式设计工作
- 行为仍在变化的大规模重构
在这些情况下,先写规格或先制定计划的提示可能更合适。
结束语
Scaffolded Test‑First Prompting 有效,因为它用可运行的目标取代了模糊的意图。
与其让 AI 猜测 “fixed” 的含义,你可以:
- 定义一个失败的示例。
- 使该示例可执行。
- 请求最小的代码改动以满足它。
这种习惯通常能让你更快收敛,得到更简洁的补丁,并减少奇怪的绕路。
如果你每天都在使用 AI 编码,这是最容易采用的工作流升级之一:先测试,后补丁,全部运行。