测试驱动 AI:让 Agent 先写测试再写实现
- 理解为什么 TDD 和 AI 是绝配:测试让 AI 有了可量化的验收标准
- 掌握"需求→AI 写测试→人工审测试→AI 写实现→跑通"的完整五步循环
- 能用一个真实小例子从零跑通 TDD+AI 流程(含可跑代码)
- 识别并避免测试太松、不审测试、迷信覆盖率这三类常见陷阱
你有没有遇到过这种情况:让 AI 帮你改了一个函数,它改完了,你用眼睛扫了一遍,感觉没问题,结果上线后另一个地方悄悄坏掉了——而那个坏掉的地方,AI 根本没意识到它动过。
这不是 AI 的智商问题,是没有护栏的问题。
解决方案其实很老——测试驱动开发(TDD)。但把它和 AI 配在一起用,才算真正把这个方法论用对了地方。
为什么 TDD 和 AI 是绝配
传统 TDD 的核心思路是:先写测试,再写实现。测试定义了"什么叫做对",实现只是为了让这个定义成立。
放到 AI 工作流里,这件事有三个额外的好处:
1. 给 AI 一个客观的完成标准
你让 AI 写代码,它写出来了,然后呢?你怎么验证它写对了?光靠人工读代码,很容易被它流畅的语法糊弄过去——代码看起来没问题,但逻辑角落里藏了个 off-by-one 或者边界条件漏处理。
但如果测试已经在那里了,"对不对"就不是靠感觉,而是靠跑一条命令。测试通过,就是通过;红了,就是红了。AI 没地方糊弄。
2. 防止修改时的悄悄回归
AI 最让人头疼的一个行为是:你让它改 A,它在改 A 的同时,顺手改了 B——也许是为了它认为的"更好",也许只是上下文关联导致的副作用。如果没有测试,这个变化是隐形的。
有了完整的测试套件,你让 AI 改完直接 pytest 或 npm test,哪里红了一目了然。
3. 强迫把需求说清楚
写测试的过程本身就是在把模糊的需求翻译成精确的断言。这个翻译工作做完,你对需求的理解会比只是"我要一个……函数"深得多——AI 也因此能得到更清晰的上下文,写出更准确的实现。
这三点加在一起,就是为什么在 SDD 规范驱动开发 里,测试是不可缺少的一环——它是把"规范"变成"机器可验"的最后一公里。
五步流程:从需求到绿灯
下面是和 AI 配合做 TDD 的完整工作流,每一步你应该做什么说清楚:
第一步:把需求说清楚(不要跳过)
在让 AI 动手之前,先用一两句话写下来:这个函数要做什么、输入是什么、输出是什么、边界条件是什么。
比如:"写一个 parse_price 函数,接受字符串 '¥1,299.00',返回浮点数 1299.0。如果字符串格式不对,抛出 ValueError。"
这一步不是废话,它决定了测试写得准不准。你跳过这步,AI 写出来的测试很可能只覆盖了最顺风顺水的路径。
第二步:让 AI 先写测试
把需求描述给 AI(在 Claude Code 里直接说,或在 Cursor 里 Cmd+K),明确告诉它:先只写测试,不要写实现。
根据下面的需求,用 pytest 写测试用例,不要写实现代码:
- 函数名:parse_price
- 输入:价格字符串,如 '¥1,299.00'、'$99.9'、'100'
- 输出:float
- 如果格式无法解析,抛出 ValueError
- 覆盖:正常输入、带货币符号、带千位符、非法输入至少各一个用例
第三步:你来审测试,不是 AI
这一步是整个流程里最容易被省略、也最不能省略的一步。
AI 写的测试,你要亲眼过一遍:
- 断言写对了吗?
assert result == 1299.0还是assert result == "1299.0"? - 边界条件覆盖了吗?空字符串、
None、纯字母有没有测? - 有没有测试写了个"总是通过"的断言——比如
assert result is not None,这种等于没测。
你对测试的理解,决定了它能不能成为真正的护栏。把审测试这步外包给 AI,等于让 AI 自己出题自己打分。
第四步:让 AI 写实现
测试审过了,再告诉 AI:
测试已经写好了,现在写 parse_price 的实现,
让 test_parse_price.py 里的所有用例通过
AI 拿着测试写实现,比"你来解释需求 AI 来猜实现"准确得多——因为测试本身就是可执行的规格。
第五步:跑测试,红转绿
让 AI 跑测试,或者你自己跑:
pytest test_parse_price.py -v
如果有红,让 AI 盯着报错修,修完再跑,直到全绿。全绿才是这一轮工作真正完成的标志,不是 AI 说"好了"。
关于更系统的 AI 任务拆解和实现节奏,参考 Plan 模式:先想清楚再动手——那里讲的"先规划再执行"和这里的"先测试再实现"是同一个思路的两个维度。
真实小例子:从零跑通
需求很简单:写一个 clamp 函数,把数字夹到 [min_val, max_val] 区间里。
需求描述(你给 AI 的)
函数 clamp(value, min_val, max_val):
- value 在区间内,原样返回
- value 小于 min_val,返回 min_val
- value 大于 max_val,返回 max_val
- min_val > max_val 时抛出 ValueError
第一步:AI 写的测试(你审过的版本)
# test_clamp.py
import pytest
def test_value_within_range():
assert clamp(5, 1, 10) == 5
def test_value_below_min():
assert clamp(-3, 0, 100) == 0
def test_value_above_max():
assert clamp(200, 0, 100) == 100
def test_value_equals_min():
assert clamp(0, 0, 10) == 0
def test_value_equals_max():
assert clamp(10, 0, 10) == 10
def test_float_values():
assert clamp(3.7, 2.5, 4.0) == pytest.approx(3.7)
def test_invalid_range_raises():
with pytest.raises(ValueError):
clamp(5, 10, 1) # min > max
审测试时要注意:test_float_values 用了 pytest.approx,如果你直接写 == 3.7 可能因为浮点精度偶发失败——这是 AI 容易写漏的细节,你审的时候要盯着。
第二步:AI 写的实现
# clamp.py
def clamp(value, min_val, max_val):
"""把 value 夹到 [min_val, max_val] 区间。"""
if min_val > max_val:
raise ValueError(
f"min_val ({min_val}) 不能大于 max_val ({max_val})"
)
return max(min_val, min(max_val, value))
第三步:跑通
在 test_clamp.py 顶部加上 from clamp import clamp,然后:
pytest test_clamp.py -v
预期输出:
test_clamp.py::test_value_within_range PASSED
test_clamp.py::test_value_below_min PASSED
test_clamp.py::test_value_above_max PASSED
test_clamp.py::test_value_equals_min PASSED
test_clamp.py::test_value_equals_max PASSED
test_clamp.py::test_float_values PASSED
test_clamp.py::test_invalid_range_raises PASSED
7 passed in 0.12s
全绿。这个例子小,但流程是真实的——换成任何复杂函数,五步不变。
故障排查表
| 症状 | 根本原因 | 解法 |
|---|---|---|
| AI 写的测试一跑就全绿,但你觉得实现不对 | 测试断言太松——比如只断言 result is not None 或 len(result) > 0 |
回头审测试,把模糊的断言改成具体值;告诉 AI "这个断言太弱,帮我改成验证具体返回值" |
| AI 改了实现后,某个原本绿的测试突然红了 | 改动引入了回归,或者 AI 顺手改了不该改的逻辑 | 不要直接让 AI 再改一遍;先 git diff 看改了什么,定位到哪行引入的,再针对性修 |
| AI 怎么改都过不了某个测试,陷入死循环 | 可能是测试本身写错了(断言了一个不可能成立的值),或需求本身有矛盾 | 停下来,把那条测试和需求描述放在一起人工推一遍逻辑;很多时候是需求本身没说清楚 |
| 测试覆盖率 100% 但线上还是出 bug | 测试只覆盖了"正常路径",漏掉了真实用户的异常输入 | 补充"破坏性测试":空字符串、超大数、None、Unicode 边界、并发等;让 AI "帮我想还有哪些边界条件没覆盖" |
| Node/Jest 版本不兼容,测试命令跑不起来 | 依赖版本问题,与 TDD 流程无关 | 跑 npx jest --version 确认版本;参考 AI 代码质量与安全 Review 里的依赖管理建议 |
这招为什么能大幅提高 AI 产出质量
回到最开始的问题:AI 产出质量为什么有时候高有时候低?
根本原因是 AI 没有办法自己验证自己的输出是否正确。它能写出语法完美、逻辑看起来合理的代码,但它无法在生成代码的同时"模拟运行"所有可能的输入。这是语言模型的结构性局限,不是某个模型的问题。
测试填的就是这个空缺:
- 测试是可执行的规格——比自然语言描述更精确,比人工 review 更可靠
- 测试覆盖的路径,AI 就必须处理——它没法对一个
assert clamp(200, 0, 100) == 100的测试装作没看到越界情况 - 测试把验证从"感觉"变成了"机器判定"——你不再需要相信 AI,你相信跑测试这个事实
这也是为什么在 AI 代码质量与安全 Review 里,测试覆盖被列为质量门控的核心指标之一——不是因为覆盖率数字好看,而是因为测试是唯一能把"这段代码应该干什么"机器化的手段。
常见问题
Q:AI 既写测试又写实现,这样 TDD 还有意义吗?
有意义,关键在你审不审测试。AI 写测试是为了帮你省时间,但测试写得对不对,你必须亲眼过一遍。如果你不审,就等于让 AI 自己出题自己答题,测试就失去了护栏的意义。TDD 的核心不是"人写测试",而是"测试先于实现存在,且由人来确认测试是对的"。
Q:追求 100% 覆盖率有意义吗?
覆盖率是指标,不是目标。覆盖率高只说明每行代码都被执行到了,不说明所有边界条件都被验证了。一个 assert result is not None 的测试可以把覆盖率顶到 100%,但什么都没真正测。更有价值的问题是:我有没有测到"用户真实会给什么输入"。
Q:每次让 AI 改代码都要跑全量测试,太慢了怎么办?
先跑和本次改动相关的测试(pytest 里用 -k 过滤,Jest 里用 --testNamePattern),改完再跑全量。CI/CD 里可以把全量测试配成每次 push 自动跑,本地只跑局部。速度和覆盖是可以平衡的,不是必须二选一。
Q:项目没有测试,能直接用 TDD+AI 吗?
可以,从新加的功能开始用。不需要把全部老代码补测试——"新代码 TDD,老代码视机会补"是最务实的推进方式。让 AI 帮你给最核心的老函数补测试,同样适用第三步"你来审"的原则。
Q:TDD 和 SDD(规范驱动)是什么关系?
SDD 讲的是在更大粒度上先写规范再动手;TDD 是在函数/模块粒度上先写测试再写实现。两者完全可以同时用:SDD 给你一个清晰的任务边界,TDD 在这个边界内保证实现的正确性。把两者结合,是目前 AI 辅助编程里最稳健的工程化路径之一。
👉 看看我们的 AI 编程实战体系课,或逛 AI 编程教程大全 把基本功打扎实。