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 · 上手陌生代码库篇

参考资料

相关推荐
kmomo..7 小时前
Linux 驱动开发学习笔记:UART、触摸屏与 ADC 驱动实战
linux·驱动开发·学习
catino8 小时前
SDD:规范驱动开发(Spec-Driven Development)
驱动开发·ai
ABB自动化1 天前
带二期控制的PowerFlex 700S驱动器
驱动开发·制造·信息与通信·模块测试·plc 控制器
额额额对了1 天前
Linux 驱动开发:基于 platform 总线 + misc 设备的 DS18B20 单总线温度驱动
linux·c语言·开发语言·驱动开发·嵌入式硬件
lhh_qrsly1 天前
三、三相霍尔传感器(BLDC / 带霍尔 PMSM)
驱动开发
幼儿园蛋小黄1 天前
嵌入式学习总结:从 I2C 到 SPI 的驱动开发实战
驱动开发·学习
老当益壮梁奶奶1 天前
驱动学习(二):从零开始:驱动工程搭建与字符设备驱动框架详解(基于IMX6ULL)
c语言·arm开发·驱动开发·嵌入式硬件·学习
虚无的纽扣1 天前
【Linux】信号(上):一个进程是怎么“被通知”的
linux·运维·驱动开发
小雪崩2 天前
嵌入式学习:Linux驱动开发总结
linux·驱动开发·学习