name: tdd-workflow description: Use when implementing any feature or bugfix, before writing implementation code - write the test first, watch it fail, write minimal code to pass; ensures tests actually verify behavior by requiring failure first. Enforces RED-GREEN-REFACTOR cycle with iron-law compliance. metadata: short-description: TDD 测试驱动开发工作流 keywords: - tdd-workflow - TDD - 测试驱动开发 - Red-Green-Refactor - 测试先行 - 单元测试 - 测试覆盖率 - test-first - test-driven category: 测试 author: Bensz Conan platform: Claude Code | OpenAI Codex | ChatGPT iron-law: | NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
TDD Workflow - 测试驱动开发工作流
与 bensz-collect-bugs 的协作约定
- 因本 skill 设计缺陷导致的 bug,先用
bensz-collect-bugs规范记录到~/.bensz-skills/bugs/,不要直接修改用户本地已安装的 skill 源码;若有 workaround,先记 bug,再继续完成任务。 - 只有用户明确要求“report bensz skills bugs”等公开上报时,才用本地
gh上传新增 bug 到huangwb8/bensz-bugs;不要 pull / clone 整个仓库。
铁律
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
违反规则的信件就是违反规则的精神。
无例外:
- 不保留为"参考"
- 写测试时"不调整"
- 不看它
- 删除=删除
常见合理化
| 借口 | 现实 |
|---|---|
| "太简单不需要测试" | 简单代码也会坏。测试只需 30 秒 |
| "我之后再测试" | 测试立即通过证明不了什么 |
| "测试后达到相同目的" | 测试后="代码做什么?"测试前="代码应该做什么?" |
| "已经手动测试了" | 临时≠系统化。无记录,无法重新运行 |
| "删除 X 小时工作是浪费" | 沉没成本谬误。保留未验证代码是技术债 |
| "保留参考,先写测试" | 你会调整它。那是测试后。删除=删除 |
红色标志 - 停止并重新开始
- 测试前有代码
- "已经手动测试了"
- "测试后达到相同目的"
- "是精神而非仪式"
- "只此一次"的合理化
- "保留为参考"
- 写测试时"不调整"
所有这些意味着:删除代码。用 TDD 重新开始。
核心理念
测试驱动开发(Test-Driven Development, TDD) 是一种先编写测试,再编写实现代码的开发方法。通过严格的 Red-Green-Refactor 循环,确保代码质量和可维护性。
TDD 循环
┌─────────────────────────────────────────────────────────┐
│ 1. RED : 编写失败的测试 │
│ 2. GREEN : 编写最简单的代码使测试通过 │
│ 3. REFACTOR: 在测试保护下重构代码 │
│ 4. 重复循环 │
└─────────────────────────────────────────────────────────┘
何时使用本技能
在以下场景时激活:
- 用户明确要求使用 TDD 或 测试驱动开发
- 需要编写新功能或修复 Bug
- 提到"测试"、"单元测试"、"测试覆盖率"
- 需要确保代码质量
- 重构现有代码(先补充测试)
TDD 工作流程
步骤 1:理解需求
在开始编码前,明确:
- 功能需求:这个功能要做什么?
- 验收标准:如何判断功能正确?
- 边界条件:有哪些特殊情况?
- 错误处理:异常情况如何处理?
步骤 2:编写失败测试(RED)
测试先行原则:
- 先写测试,不写实现
- 运行测试,确认失败(证明测试有效)
- 阅读错误信息,理解预期
测试命名规范(AAA 模式):
# Should_预期行为_When_测试条件
def should_return_user_when_id_exists():
# Arrange(准备)
user_id = 123
expected_user = User(id=123, name="Alice")
# Act(执行)
result = user_service.get_by_id(user_id)
# Assert(断言)
assert result.id == expected_user.id
assert result.name == expected_user.name
步骤 3:最小化实现(GREEN)
最简单的可工作代码:
- 只写足够使测试通过的代码
- 不追求完美,追求通过
- 硬编码可以接受(第一步)
# 最初版本 - 硬编码也可以
def get_by_id(user_id):
if user_id == 123:
return User(id=123, name="Alice")
return None
步骤 4:运行测试确认通过
# 运行测试
pytest tests/test_user_service.py -v
# 期望输出
✅ should_return_user_when_id_exists PASSED
步骤 5:重构代码(REFACTOR)
在测试保护下优化:
- 消除重复
- 提取方法
- 改善命名
- 优化结构
# 重构后版本
def get_by_id(user_id):
return _user_repository.find_by_id(user_id)
步骤 6:重复循环
每个功能点重复上述步骤,直到功能完整。
测试质量标准
必须遵守的规则
- ✅ 测试覆盖率 ≥ 80%
- ✅ 每个测试用例独立(不依赖其他测试)
- ✅ 测试可重复(多次运行结果一致)
- ✅ 测试命名清晰(描述意图)
- ✅ 遵循 AAA 模式(Arrange-Act-Assert)
禁止的反模式
- ❌ 伪测试:测试代码没有断言
- ❌ 万能测试:一个测试验证太多东西
- ❌ 测试内部实现:应该测试行为,不是实现细节
- ❌ 脆弱测试:依赖外部状态(时间、随机数等)
TDD 最佳实践
1. 小步前进
- 一次只写一个测试
- 一次只实现一个功能点
- 频繁运行测试(每 1-2 分钟)
2. 测试隔离
# 好的示例 - 使用 fixtures
@pytest.fixture
def clean_database():
db.reset()
yield
db.cleanup()
def test_create_user(clean_database):
user = user_service.create("Alice")
assert user.name == "Alice"
3. 测试边界条件
def test_get_by_id():
# 正常情况
assert get_user(1) is not None
# 边界条件
assert get_user(0) is None
assert get_user(-1) is None
assert get_user(999999) is None
4. 测试异常情况
def test_create_user_with_duplicate_email():
with pytest.raises(DuplicateEmailError):
user_service.create("alice@example.com")
user_service.create("alice@example.com")
不同语言的 TDD 示例
Python (pytest)
# 测试
def should_calculate_total_price():
cart = ShoppingCart()
cart.add_item(Item(name="Book", price=10))
cart.add_item(Item(name="Pen", price=5))
assert cart.total_price() == 15
# 实现
class ShoppingCart:
def __init__(self):
self.items = []
def add_item(self, item):
self.items.append(item)
def total_price(self):
return sum(item.price for item in self.items)
JavaScript (Jest)
// 测试
test('should calculate total price', () => {
const cart = new ShoppingCart();
cart.addItem({ name: 'Book', price: 10 });
cart.addItem({ name: 'Pen', price: 5 });
expect(cart.totalPrice()).toBe(15);
});
// 实现
class ShoppingCart {
constructor() {
this.items = [];
}
addItem(item) {
this.items.push(item);
}
totalPrice() {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
}
TypeScript (Jest)
// 测试
test('should calculate total price', () => {
const cart = new ShoppingCart();
cart.addItem({ name: 'Book', price: 10 });
cart.addItem({ name: 'Pen', price: 5 });
expect(cart.totalPrice()).toBe(15);
});
// 实现
interface Item {
name: string;
price: number;
}
class ShoppingCart {
private items: Item[] = [];
addItem(item: Item): void {
this.items.push(item);
}
totalPrice(): number {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
}
常见问题
Q1: 是否需要 100% 测试覆盖率?
A: 不一定。80-90% 是合理目标。以下情况可以例外:
- UI 组件(优先用 E2E 测试)
- 简单的 getter/setter
- 第三方库的封装
Q2: 如何测试私有方法?
A: 不要直接测试私有方法。应该通过公共接口测试其行为。如果私有方法太复杂,考虑提取到独立的类。
Q3: TDD 会降低开发速度吗?
A: 短期可能稍慢,但长期来看:
- 减少调试时间
- 减少回归 Bug
- 提高代码可维护性
- 整体效率提升 30-50%
Q4: 什么时候不适合 TDD?
A:
- 探索性原型(POC)
- UI 设计探索
- 紧急热修复(但仍应事后补充测试)
验证清单
完成 TDD 开发后,检查:
- 所有测试通过
- 测试覆盖率 ≥ 80%
- 每个测试用例独立且可重复
- 测试命名清晰(Should_ExpectedBehavior_When_StateUnderTest)
- 遵循 AAA 模式
- 无伪测试(所有测试都有断言)
- 边界条件已测试
- 异常情况已测试