Pi实战 12:TDD 测试驱动篇——用 Pi 做测试驱动开发

Pi实战 12:TDD 测试驱动篇------用 Pi 做测试驱动开发

来源:Daniel Weinmann、Marcos Lobo、tweag《Agentic Coding Handbook》、VS Code TDD 指南,以及 Martin Fowler 对"Agent 循环内 TDD"的质疑(保持客观)。


一、使用场景

  • 让 Agent 加新功能,结果它悄悄改了旧行为还没人发现。
  • 改完不敢动,怕回退,因为没测试兜底。
  • 想要"Agent 写的代码至少行为是对的",而不是靠人眼 review 每一行。
  • 想有一个清晰的"完成"标准,而不是一句"改好了"。

二、能解决什么问题

TDD(测试驱动开发)给 Agent 一个不会骗人的验收标准:先写一个会失败的测试(定义"什么算对"),再让 Agent 写代码把它跑绿。

它顺带解决两件别的事:

  • 逼你澄清需求:写测试的过程就是想把"这段代码到底该干嘛"讲清楚的过程。
  • 给后续重构兜底:有测试在,你(和 Agent)以后改代码才有安全感。

三、解决思路

思路 1:红 - 绿 - 重构的最小循环

经典 TDD 三步,对 Agent 一样适用:

复制代码
1. Red    ------ 写一个会失败的测试(此时功能还没实现)
2. Green  ------ 让 Agent 写"刚好能让测试通过"的最少代码
3. Refactor------ 保持测试绿的前提下,清理代码

tweag 的《Agentic Coding Handbook》点出它和 Agent 特别搭:

"It keeps you in flow: Tests give you checkpoints. Instead of stopping to debug a vague output, you just write the next test and let the AI catch up.

It reinforces clean, behavioral thinking: TDD forces you to describe what the code should do, not how to write it. That's exactly how we should prompt LLMs."

(测试给你检查点,让你保持心流:不用停下来调试模糊的输出,写下一个测试让 AI 追上就行。它还强化"行为化"思维:TDD 逼你描述代码该做什么 ,而不是怎么写------而这正是我们该给 LLM 的提示方式。)

思路 2:你写验收测试,Agent 负责实现

Marcos Lobo 的极简工作流(最适合起步):

复制代码
1. 你写验收测试(acceptance tests)
2. 让 Agent 实现让这些测试通过的代码
3. 你 review 生成的代码,然后迭代

思路 3:把"完成"定义死

Daniel Weinmann 的 Definition of Done,直接写进 AGENTS.md:

复制代码
- TDD is non-negotiable. Always write the tests first and ensure they fail before implementing.
  Move in baby steps through the red-green-refactor cycles.
- A task is not done if the linter is not passing for the whole project.
- A task is not done if it has new behavior without tests to ensure that new behavior.

翻译:

  • TDD 不可商量:永远先写测试、确认它失败,再实现;小步快跑红-绿-重构。
  • 整个项目 linter 没过 = 没完成。
  • 有新行为却没对应测试 = 没完成。

四、如何实战

实战 0(手把手):从零走一遍"红 - 绿 - 重构"

前面讲的都是概念。下面用一段能直接复制、能真跑的例子,把整个循环从头走一遍。我们用 Python + pytest(最直观,换成 JS/Go/Java 逻辑一模一样)。

场景 :写一个"购物车结算"函数 calculate_total(items),规则是"各商品 price×qty 求和,满 100 减 10"。

准备(一次就好):

bash 复制代码
mkdir tdd-demo && cd tdd-demo
pip install pytest        # 装测试框架;JS 项目就用 npm install -D jest/vitest
touch cart.py             # 先建一个空文件

下面每一步,我们都先写测试 → 跑测试看它红 → 写最少代码 → 跑测试看它绿 。命令统一用 pytest -q(-q 表示安静模式,红的绿的很清楚)。


第 1 步:写第一个测试(空购物车 = 0)

test_cart.py:

python 复制代码
from cart import calculate_total

def test_empty_cart_is_zero():
    assert calculate_total([]) == 0

跑:

bash 复制代码
$ pytest -q

输出是红 的,因为 calculate_total 还不存在:

复制代码
E   ImportError: cannot import name 'calculate_total' from 'cart'

白话 :测试一跑就红,这正是我们想要的。它证明"这个测试真的在测东西"------如果这时候它就绿了,反而说明测试没测到点上。先确认它会失败,是 TDD 的第一铁律。


第 2 步:写"最蠢"的代码让它绿

cart.py:

python 复制代码
def calculate_total(items):
    return 0

跑 pytest -q → 绿。

白话 :别笑 return 0 太蠢。TDD 的精髓就是先迈最小的一步:现在框架通了、函数名对了、空购物车这个用例过了。这一步的价值是"确认地基没问题",不是"写出完整逻辑"。


第 3 步:加第二个测试(一件商品)

在 test_cart.py 追加:

python 复制代码
def test_single_item():
    assert calculate_total([(10, 2)]) == 20   # 单价 10,买 2 件

跑 pytest -q → 又红 了(return 0 不等于 20)。

白话:新测试红是正常的、是好事------说明它在真正干活,逼着我们往下写。


第 4 步:让实现"刚好"满足所有测试

cart.py:

python 复制代码
def calculate_total(items):
    total = 0
    for price, qty in items:
        total += price * qty
    return total

跑 pytest -q → 全绿。

白话 :这段逻辑是被测试一步步逼出来的,不是凭空拍的。这就是 TDD 的核心体验------"测试告诉你要写什么,你只写让测试过的那一点"。


第 5 步:加"满减"测试(满 100 减 10)

test_cart.py 追加:

python 复制代码
def test_discount_over_100():
    assert calculate_total([(60, 1), (60, 1)]) == 110   # 共 120,满 100 减 10

跑 pytest -q → 红(现在算出 120,不是 110)。

白话:这个失败精准地告诉你"满减规则还没实现",而且只告诉你这一件事。


第 6 步:补上满减逻辑

cart.py:

python 复制代码
def calculate_total(items):
    total = 0
    for price, qty in items:
        total += price * qty
    if total >= 100:
        total -= 10
    return total

跑 pytest -q → 全绿。功能齐了。


第 7 步:重构(不改变行为,只让代码更干净)

回头看代码:100、10 这种"魔术数字"埋在代码里,别人看不懂规则。把它们提成常量:

cart.py:

python 复制代码
DISCOUNT_THRESHOLD = 100   # 满减门槛
DISCOUNT_AMOUNT = 10       # 减免额

def calculate_total(items):
    total = sum(price * qty for price, qty in items)
    if total >= DISCOUNT_THRESHOLD:
        total -= DISCOUNT_AMOUNT
    return total

跑 pytest -q → 还是全绿。

白话 :重构不改任何行为 ,只是让代码更好读。为什么敢放心改?因为有测试兜底------万一改坏了,pytest 立刻变红提醒你。这就是测试给重构提供的"安全感"。


这一遍走下来你得到了什么?

每一步都被测试推着走,最后的代码是被一整串测试证明过 的。以后谁想改 calculate_total,只要不跑绿测试,就不算改完------这就是给 Agent(和人)的"不会骗人的验收标准"。

实战 1:把上面交给 Pi,用 checkbox 计划驱动它

看懂循环后,就可以让 Pi 自己跑了。把刚才那套写成带勾选框的 TDD 计划丢给它,它会一项项"先测试后实现":

markdown 复制代码
# TDD Plan: 购物车结算 calculate_total

用 pytest,严格红-绿-重构,每实现一步都要跑 pytest 确认变绿。

- [ ] 写测试:空购物车返回 0;先跑,确认它红
- [ ] 写最少代码让它绿
- [ ] 写测试:单件商品 (price, qty) 返回 price × qty;确认红
- [ ] 实现求和逻辑,跑测试确认绿
- [ ] 写测试:满 100 减 10(例如 [(60,1),(60,1)] == 110);确认红
- [ ] 实现满减,跑测试确认绿
- [ ] 重构:把 100 / 10 提成常量,跑测试确认仍绿

关键:让 Pi "每步都跑测试并贴结果给我看",你只当那个"看红了没、绿了没"的人。

实战 2:分步下指令------把红/绿/重构拆成三步

不想一次给一整份计划,也可以对 Pi 分三次下指令,逼它一次只干一件事(这是最不容易跑偏的用法):

复制代码
第 1 句:先只写「空购物车返回 0」和「单件商品返回 price×qty」这两个失败测试,
        别写实现,写完后跑 pytest 把红色的报错贴给我。

(你确认测试确实红了、且测的是你想要的行为之后)

第 2 句:现在写最少代码让这两个测试变绿,跑 pytest 把绿色结果贴给我。

(变绿之后)

第 3 句:在测试保持全绿的前提下,重构这段代码让它更好读,再跑一次确认还是绿的。

一次只让它做"写测试 / 写实现 / 重构"中的一件,比一句话"用 TDD 帮我加个功能"稳得多------后者它经常假装在做 TDD,其实一把梭。

实战 3:先审测试,再让它写实现(human-in-the-loop)

最稳妥的一道保险:让 Pi 先把测试写给你看,你点头了它再写实现:

复制代码
你先只写测试,把测试代码贴给我。
我确认测试覆盖了我想要的所有行为之后,你再写让这些测试通过的代码。
实现过程中如果测试红了,先把红色报错贴给我,别自己闷头瞎试。

这样即使 Pi 理解偏了,也只会偏在"测试"这一层(改几行测试就行),不会连带写出一大坨错的实现让你返工。在测试层拦跑偏,比在实现层拦省力得多。


五、一个必须告诉你的争议(保持客观)

TDD 在 Agent 循环里是不是真有价值,社区是有争论的,别把它当圣经。

Martin Fowler 在 2026 年的文章里坦率地说:

"I personally have stopped telling my coding agents to write tests first, let alone do TDD... until I see evals or other strong arguments that convince me otherwise. I'm trying to focus instead on the benefits of TDD when I use it outside the agent loop."

(我个人已经不再让我的编码 Agent"先写测试"了,更别提完整做 TDD------除非我看到 evals 或其他强有力的证据。我现在更关注在Agent 循环之外用 TDD 带来的好处。)

怎么理解这个分歧:

  • 在 Agent 循环内强制每一步都红-绿-重构,可能是"仪式感大于价值"(theater),甚至拖慢它。
  • 在 Agent 循环外 ,TDD 的价值很实在:你用测试当验收标准、当重构安全网、当回归测试------让 Agent 跑你的测试,看它有没有破坏既有行为。

给你的可操作结论:

  • 不必迷信"每一步都严格 TDD"。
  • 至少做到:新行为要有测试覆盖(Daniel 的 Definition of Done)、用既有测试 suite 当 Agent 的回归护栏。
  • 想让 Agent "写测试优先"时,用 checkbox 计划 + 先审测试再实现,最稳。

六、小结 & 避坑

  • 红-绿-重构是最小循环;测试是给 Agent 的"不会骗人的验收标准"。
  • 最简起步:你写验收测试 → Agent 实现 → 你 review → 迭代。
  • 定义死"完成":linter 没过 / 新行为无测试 = 没完成。
  • 先审测试再实现:在测试层拦跑偏,比拦实现更省力。
  • 别迷信循环内 TDD:Fowler 已不再强制 Agent 每步 TDD;把测试当循环外的验收/回归护栏更实在。

上一篇:11 · 上手陌生代码库篇

参考资料

相关推荐
此生决int2 天前
Linux系列(08)——进程控制
linux·运维·驱动开发
女神下凡3 天前
芯参谋(85):Linux_PPI_NAND_驱动开发指南
linux·运维·arm开发·驱动开发·单片机·嵌入式硬件·设计规范
世微 如初4 天前
24V 输入转 9V/0.8A 输出:基于 AP5190 的 LED 降压恒流驱动方案设计与 BOM 清单
驱动开发·芯片·led电源驱动·降压恒流ic
mg6684 天前
安卓玩机工具----告别面具!SukiSU Ultra 内核级 Root 与终极隐藏实战指南
驱动开发·root系统
女神下凡5 天前
芯参谋(87):Linux 系统(SPI NOR)驱动开发指南
linux·运维·arm开发·驱动开发·嵌入式硬件·设计规范
cynicism??6 天前
Linux 与 ZXISOS 驱动开发架构对比
linux·驱动开发·架构
女神下凡6 天前
芯参谋(83):AliOS Things_驱动开发指南
arm开发·驱动开发·单片机·嵌入式硬件·设计规范·1024程序员节
万物智能信息科技6 天前
指纹识别传感器驱动开发—【万物智能之开源鸿蒙OpenHarmony系统实战开发系列教程】
驱动开发·华为·开源·harmonyos·鸿蒙
女神下凡6 天前
芯参谋(84):Linux eMMC 驱动开发指南
linux·运维·arm开发·驱动开发·单片机·嵌入式硬件