正在加载,请稍候…

测试驱动开发:红绿重构实战

通过红绿重构循环学习TDD。使用TypeScript和Jest的实际示例,编写更好的测试并驱动设计。

测试驱动开发:红绿重构实战

测试驱动开发:红绿重构实战

TDD 意味着在编写代码之前先编写测试,通过测试驱动设计。

TDD 循环

  1. :编写一个失败的测试
  2. 绿:编写最少的代码使其通过
  3. 重构:改进代码,同时保持测试通过

测试驱动开发:红绿重构实战 插图

示例:构建购物车

步骤 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 产生的代码既经过良好测试,又设计精良。