Vibe Coding + TDD:用一份 CLAUDE.md 让 AI 全自动走完测试驱动开发
先说结论
Vibe coding 让写代码的门槛降到了地板,但也让代码质量降到了地下室。
AI 生成代码的速度已经不是瓶颈了,验证代码的正确性才是。你丢一句需求给 AI,它哗哗吐出几百行代码,编译过了,简单 case 也过了——然后上线炸了。
问题出在哪?你没有告诉 AI "正确"长什么样,也没有约束它的开发习惯。
答案是 TDD + CLAUDE.md。TDD 定义"正确",CLAUDE.md 约束"习惯"。两个加起来,你只需要说一句话,AI 就能全自动走完"写测试 → 确认失败 → 写实现 → 跑测试 → 跑 clippy"的完整流程。
真实的自动化流程长什么样
别信那些"你需要手动写测试骨架"的说法。真实的工作流是这样的:
你只需要说一句话:
写一个令牌桶限流器,先写测试。
然后 AI 全自动完成以下所有步骤:
第一步:写测试(带 todo!() 占位)
AI 创建 src/error.rs,定义错误类型:
use thiserror::Error;
#[derive(Debug, Error, PartialEq, Eq)]
pub enum RateLimiterError {
#[error("no tokens available, retry after {retry_after_ms}ms")]
NoTokensAvailable { retry_after_ms: u64 },
}然后创建 src/lib.rs,写好测试,实现用 todo!() 占位:
mod error;
pub use error::RateLimiterError;
pub struct TokenBucket {
capacity: u32,
refill_rate: f64,
available: f64,
last_refill: std::time::Instant,
}
impl TokenBucket {
pub fn new(capacity: u32, refill_rate: f64) -> Self {
todo!()
}
pub fn try_acquire(&mut self) -> Result<(), RateLimiterError> {
todo!()
}
pub fn available_tokens(&mut self) -> u32 {
todo!()
}
fn refill(&mut self) {
todo!()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_bucket_starts_full() { /* ... */ }
#[test]
fn test_try_acquire_success() { /* ... */ }
#[test]
fn test_try_acquire_rejects_when_empty() { /* ... */ }
#[test]
fn test_refill_over_time() { /* ... */ }
// ... 更多测试
}第二步:确认测试因 todo!() panic
AI 自动跑 cargo test,确认 8 个测试全部 panic——这是 TDD 的"红灯"阶段。
8 个测试全部因 todo!() panic,符合预期。
第三步:实现代码,让测试变绿
AI 把 todo!() 替换成真正的实现:
impl TokenBucket {
pub fn new(capacity: u32, refill_rate: f64) -> Self {
Self {
capacity,
refill_rate,
available: capacity as f64,
last_refill: std::time::Instant::now(),
}
}
pub fn try_acquire(&mut self) -> Result<(), RateLimiterError> {
self.refill();
if self.available >= 1.0 {
self.available -= 1.0;
Ok(())
} else {
let retry_after_ms = ((1.0 - self.available) / self.refill_rate * 1000.0).ceil() as u64;
Err(RateLimiterError::NoTokensAvailable { retry_after_ms })
}
}
pub fn available_tokens(&mut self) -> u32 {
self.refill();
self.available.floor() as u32
}
fn refill(&mut self) {
let now = std::time::Instant::now();
let elapsed = now.duration_since(self.last_refill).as_secs_f64();
self.available = (self.available + elapsed * self.refill_rate).min(self.capacity as f64);
self.last_refill = now;
}
}第四步:跑测试,确认全绿
cargo test → 8 个测试全部通过
第五步:跑 clippy,确认无警告
cargo clippy → 无警告
第六步:给你一个漂亮的总结表格
┌───────────────────────────────────────┬──────────────┐
│ 测试 │ 场景 │
├───────────────────────────────────────┼─────────────┤
│ test_new_bucket_starts_full │ 初始状态 │
│ test_try_acquire_success │ 正常消费 │
│ test_try_acquire_rejects_when_empty │ 桶空拒绝 │
│ test_refill_over_time │ 时间补充 │
│ test_refill_does_not_exceed_capacity │ 不超容量 │
│ test_zero_capacity_always_rejects │ 零容量边界 │
│ test_retry_after_ms_is_reasonable │ 重试时间计算 │
│ test_multiple_acquires_deplete_bucket │ 连续耗尽 │
└───────────────────────────────────────┴─────────────┘
全程你只说了一句话。 测试写了、实现了、跑过了、clippy 检查了——全自动。
为什么能做到全自动?因为有 CLAUDE.md
上面那个流程之所以能跑通,不是因为 AI 聪明,而是因为 CLAUDE.md 里写好了规则。
AI 每次启动都会读 CLAUDE.md,它就像一份"项目级的工作手册"。你把 TDD 流程写进去,AI 就会自动遵守。
我的 CLAUDE.md 长这样:
# 项目名称
## 开发流程
本项目严格遵循 TDD(测试驱动开发)流程:
1. **先写测试,再写实现** — 不允许出现 `todo!()` 占位的实现
2. **测试必须先跑失败** — 确认测试在检验真实行为
3. **最小实现让测试通过** — 不要过度设计
4. **重构时测试必须全绿** — 改完代码第一件事跑 `cargo test`
## 代码规范
- **错误处理**:禁止 `.unwrap()` 和 `.expect()`(测试代码除外)
- 库代码用 `thiserror` 定义错误类型
- 应用代码用 `anyhow` 传播错误
- 每个 `?` 都要考虑调用者需要什么上下文
- **数据模型**:优先用 enum 而不是 String 表示有限集合
- **命名**:测试函数用英文 snake_case,注释用中文说明"为什么测这个"
- **并发**:共享状态用 `Arc<Mutex<T>>` 或 `Arc<RwLock<T>>`,禁止 `Rc`
## 测试规范
- 单元测试放在 `#[cfg(test)] mod tests` 里
- 集成测试放在 `tests/` 目录下
- 测试命名格式:`test_<场景>_<期望行为>`
- 每个公开函数至少覆盖:正常路径、错误路径、边界条件
- 涉及数据变换的模块必须有 proptest roundtrip 测试
## 依赖选择
- 解析:`nom`(不用正则)
- 错误:`thiserror`(库)/ `anyhow`(应用)
- 序列化:`serde` + `serde_json`
- 测试:`proptest`(property-based)、`tempfile`(临时文件)
- 日志:`tracing`(不用 `println!`)
## 项目结构
src/
├── lib.rs # 库入口
├── error.rs # 错误类型定义
├── types.rs # 核心数据模型
tests/ # 集成测试
fixtures/ # 测试数据就这些。没有什么花哨的东西,但每一行都在约束 AI 的行为。
这份 CLAUDE.md 怎么来的
不是我一开始就写好的。是从踩坑中一点点长出来的。
第一次迭代:加了"禁止 unwrap"。 AI 写的代码到处 .unwrap(),上线后 panic 不断。加了这条规则之后,AI 开始用 ? 和 Result,错误处理质量肉眼可见地提高了。
第二次迭代:加了"先写测试"。 AI 一口气写 200 行实现,测试一个没有。加了 TDD 流程之后,AI 会先生成测试骨架,再填充实现——代码结构天然更清晰。
第三次迭代:加了"错误类型用 thiserror"。 AI 喜欢用 String 当错误类型,调用者完全不知道怎么处理。加了这条之后,错误类型变成了 enum,编译器帮你检查所有分支。
第四次迭代:加了测试规范。 AI 写的测试要么测的是实现细节,要么覆盖率看着高但全是 happy path。加了"每个函数覆盖正常路径、错误路径、边界条件"之后,测试质量上了一个台阶。
每次踩坑就加一条规则,CLAUDE.md 就是你的血泪经验的固化。
CLAUDE.md 的进阶用法
领域特化
如果你在做 Web 服务,可以加:
## Web 服务规范
- 框架:axum(不用 actix-web)
- 状态管理:`State<Arc<AppState>>`,禁止在 state 里用 `Rc`
- 中间件:错误处理用 `tower::ServiceBuilder`,不用手写
- 数据库:`sqlx`(编译期检查 SQL),不用 `diesel`如果做 CLI 工具:
## CLI 规范
- 参数解析:`clap` derive 模式
- 输出:结构化日志用 `tracing`,用户可见输出用 `eprintln!`(错误)/ `println!`(正常)
- 退出码:0 成功,1 用户错误,2 系统错误代码审查清单
## 代码审查
提交前检查:
- [ ] `cargo test` 全绿
- [ ] `cargo clippy` 无警告
- [ ] 没有新增 `.unwrap()`(测试除外)
- [ ] 新增的公开函数有测试覆盖
- [ ] 错误类型用 `thiserror` 定义,不是 `String`这些清单会让 AI 在写完代码后自动检查,而不是等你手动 review 才发现问题。
反模式黑名单
## 禁止的写法
- ❌ `.unwrap()` 在非测试代码中
- ❌ `String` 当错误类型
- ❌ `println!` 做日志(用 `tracing`)
- ❌ `clone()` 来绕过借用检查(先想清楚所有权)
- ❌ `unsafe` 除非有注释说明为什么安全黑名单比白名单更有效。 你告诉 AI"不要做什么"比"要做什么"更容易执行。
为什么 CLAUDE.md 比口头约束更有效
你可能会说:"我每次对话跟 AI 说'先写测试'不就行了?"
不行。原因有三个:
一致性。 你不可能每次都记得说全所有规范。CLAUDE.md 不会忘。
权威性。 你说"先写测试",AI 可能听了也可能没听。CLAUDE.md 里的规则是项目级的,AI 会把它当成"这个项目的规矩"来遵守。
可迭代。 你踩一次坑,加一条规则,CLAUDE.md 就越来越完善。口头约束没有积累效应。
我的 CLAUDE.md 从最初的 3 行长到了现在的 50 多行,每一行都是一个真实的踩坑经验。
实用建议:从零开始写你的 TDD 版 CLAUDE.md
不需要一步到位。从这四条开始:
# 项目名称
## 开发流程
1. 先写测试,再写实现
2. 禁止 `.unwrap()`(测试除外)
3. 错误类型用 `thiserror`
4. 每个公开函数至少覆盖:正常路径、错误路径、边界条件就这四条,已经能大幅提升 AI 生成代码的质量了。
然后每次踩坑就加一条。两周之后你会发现,CLAUDE.md 已经是你项目里最有价值的文件之一——不是因为它写了什么高深的东西,而是因为它把你的经验和教训固化成了 AI 能理解的规则。
结论
Vibe coding 不是敌人,TDD 也不是银弹。但在 Rust 的世界里,这三者——CLAUDE.md 的行为约束 + TDD 的正确性定义 + AI 的实现速度——结合产生的化学反应比我预期的要强得多。
Rust 的类型系统是第一道防线,测试是第二道,CLAUDE.md 是第三道——它确保 AI 不会在前两道防线上偷懒。
关键是你要转变一个观念:CLAUDE.md 不是项目简介,而是你给 AI 的工作手册。 写得越详细,AI 的输出越靠谱。
下次开一个 Rust 项目之前,先花 10 分钟写一份 TDD 版的 CLAUDE.md。然后你只需要说一句话,AI 就能全自动走完"写测试 → 确认失败 → 写实现 → 跑测试 → 跑 clippy"的完整流程。
这 10 分钟能省你后面 10 小时的 debug 时间。