正在加载,请稍候…

测试驱动开发(TDD):红-绿-重构实战

通过红-绿-重构循环学习TDD,理解先写测试如何带来更好的设计、更高的信心和更整洁的代码。

测试驱动开发(TDD):红-绿-重构实战

测试驱动开发(TDD):红-绿-重构实战

TDD 颠覆了传统流程:先编写一个会失败的测试,然后编写刚好能让测试通过的代码。

红-绿-重构循环

  1. :编写一个描述期望行为的失败测试
  2. 绿:编写最简单的代码使其通过
  3. 重构:在不破坏测试的前提下清理代码

测试驱动开发(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;
}

测试驱动开发(TDD):红-绿-重构实战示意图

步骤 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):红-绿-重构实战示意图

由外而内的 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 的纪律在长期维护、多人协作的代码库中回报最大。