
测试驱动开发:红绿重构实战
TDD 意味着在编写代码之前先编写测试,通过测试驱动设计。
TDD 循环
- 红:编写一个失败的测试
- 绿:编写最少的代码使其通过
- 重构:改进代码,同时保持测试通过
示例:构建购物车
步骤 1:红 - 编写失败的测试
// cart.test.ts
import { ShoppingCart } from './cart';
describe('ShoppingCart', () => {
let cart: ShoppingCart;
beforeEach(() => {
cart = new ShoppingCart();
});
test('starts empty', () => {
expect(cart.itemCount).toBe(0);
expect(cart.total).toBe(0);
});
test('adds items', () => {
cart.add({ id: '1', name: 'Apple', price: 1.50 });
expect(cart.itemCount).toBe(1);
});
test('calculates total', () => {
cart.add({ id: '1', name: 'Apple', price: 1.50 });
cart.add({ id: '2', name: 'Banana', price: 0.75 });
expect(cart.total).toBeCloseTo(2.25);
});
test('applies discount for items over 5', () => {
for (let i = 0; i < 6; i++) {
cart.add({ id: `${i}`, name: `Item ${i}`, price: 10 });
}
expect(cart.total).toBeLessThan(60); // discount applied
});
});
步骤 2:绿 - 最小实现
// cart.ts
interface Product {
id: string;
name: string;
price: number;
}
export class ShoppingCart {
private items: Product[] = [];
add(product: Product): void {
this.items.push(product);
}
get itemCount(): number {
return this.items.length;
}
get total(): number {
const subtotal = this.items.reduce((sum, item) => sum + item.price, 0);
return this.items.length > 5 ? subtotal * 0.9 : subtotal;
}
}
步骤 3:重构
// After tests pass, improve the design
export class ShoppingCart {
private items: Map<string, { product: Product; quantity: number }> = new Map();
add(product: Product, quantity = 1): void {
const existing = this.items.get(product.id);
if (existing) {
existing.quantity += quantity;
} else {
this.items.set(product.id, { product, quantity });
}
}
remove(productId: string): void {
this.items.delete(productId);
}
get itemCount(): number {
return Array.from(this.items.values())
.reduce((sum, { quantity }) => sum + quantity, 0);
}
get subtotal(): number {
return Array.from(this.items.values())
.reduce((sum, { product, quantity }) => sum + product.price * quantity, 0);
}
get total(): number {
return this.itemCount > 5 ? this.subtotal * 0.9 : this.subtotal;
}
}
测试边界
// Test at the right level - avoid over-mocking
describe('UserRegistrationService', () => {
// Use real in-memory repository, not mocked
let userRepo: InMemoryUserRepository;
let service: UserRegistrationService;
beforeEach(() => {
userRepo = new InMemoryUserRepository();
service = new UserRegistrationService(userRepo, new FakeEmailService());
});
test('registers new user', async () => {
const result = await service.register('alice@example.com', 'password');
expect(result.success).toBe(true);
const user = await userRepo.findByEmail('alice@example.com');
expect(user).toBeDefined();
});
test('rejects duplicate email', async () => {
await service.register('alice@example.com', 'password');
const result = await service.register('alice@example.com', 'other');
expect(result.success).toBe(false);
expect(result.error).toContain('already exists');
});
});
重构中的 TDD
// First: write characterization tests for existing code
test('current behavior (before refactor)', () => {
const result = legacyCalculate(10, 5);
expect(result).toBe(42); // document what it actually does
});
// Then refactor with confidence
TDD 产生的代码既经过良好测试,又设计精良。