测试驱动开发(TDD):红-绿-重构实战
TDD 颠覆了传统流程:先编写一个会失败的测试,然后编写刚好能让测试通过的代码。
红-绿-重构循环
- 红:编写一个描述期望行为的失败测试
- 绿:编写最简单的代码使其通过
- 重构:在不破坏测试的前提下清理代码
实战示例:购物车
步骤 1:红 - 编写失败的测试
// cart.test.ts
import { ShoppingCart } from './cart';
describe('ShoppingCart', () => {
it('should start empty', () => {
const cart = new ShoppingCart();
expect(cart.items).toHaveLength(0);
expect(cart.total).toBe(0);
});
});
步骤 2:绿 - 最小实现
// cart.ts
export class ShoppingCart {
items: never[] = [];
total = 0;
}
步骤 3:再次红 - 添加更多行为
it('should add items', () => {
const cart = new ShoppingCart();
cart.addItem({ id: '1', name: 'Book', price: 15.99, quantity: 2 });
expect(cart.items).toHaveLength(1);
expect(cart.total).toBe(31.98);
});
步骤 4:绿
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
}
export class ShoppingCart {
private _items: CartItem[] = [];
get items(): CartItem[] { return [...this._items]; }
get total(): number {
return this._items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
addItem(item: CartItem): void {
const existing = this._items.find(i => i.id === item.id);
if (existing) {
existing.quantity += item.quantity;
} else {
this._items.push({ ...item });
}
}
}
继续:移除商品
it('should remove items', () => {
const cart = new ShoppingCart();
cart.addItem({ id: '1', name: 'Book', price: 15.99, quantity: 1 });
cart.removeItem('1');
expect(cart.items).toHaveLength(0);
});
it('should apply discount', () => {
const cart = new ShoppingCart();
cart.addItem({ id: '1', name: 'Book', price: 100, quantity: 1 });
cart.applyDiscount(10); // 10% off
expect(cart.total).toBe(90);
});
由外而内的 TDD(伦敦学派)
// 从外部开始:使用 mock 的控制器测试
describe('POST /cart/items', () => {
it('should add item to cart', async () => {
const mockCartService = { addItem: jest.fn().mockResolvedValue(updatedCart) };
const controller = new CartController(mockCartService);
const req = { body: { itemId: '1', quantity: 2 }, user: { id: 'user1' } };
const res = { json: jest.fn() };
await controller.addItem(req, res);
expect(mockCartService.addItem).toHaveBeenCalledWith('user1', '1', 2);
expect(res.json).toHaveBeenCalledWith(updatedCart);
});
});
何时使用 TDD
非常适合:
- 业务逻辑和算法
- 工具函数和纯函数
- 复杂状态机
不太适用:
- UI 组件(优先使用快照/交互测试)
- 数据库迁移
- 第三方集成
TDD 的好处
- 设计压力:难以测试的代码往往设计不佳
- 文档:测试描述了期望的行为
- 信心:重构时无需担心
- 覆盖率:按定义,所有代码都有测试
TDD 的纪律在长期维护、多人协作的代码库中回报最大。