正在加载,请稍候…

Test-Driven Development (TDD): Red-Green-Refactor in Practice

Learn TDD by practicing the Red-Green-Refactor cycle. Understand how writing tests first leads to better design, higher confidence, and cleaner code.

Test-Driven Development (TDD): Red-Green-Refactor in Practice

TDD flips the script: write a failing test first, then write just enough code to pass it.

The Red-Green-Refactor Cycle

  1. Red: Write a failing test that describes desired behavior
  2. Green: Write the simplest code to make it pass
  3. Refactor: Clean up without breaking tests

Practical Example: Shopping Cart

Step 1: Red - Write failing test

// 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);
  });
});

Step 2: Green - Minimal implementation

// cart.ts
export class ShoppingCart {
  items: never[] = [];
  total = 0;
}

Step 3: Red again - Add more behavior

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);
});

Step 4: Green

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 });
    }
  }
}

Continue: Remove 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 with Outside-In (London School)

// Start from the outside: controller test with mocks
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);
  });
});

When to Use TDD

Great for:

  • Business logic and algorithms
  • Utilities and pure functions
  • Complex state machines

Less useful for:

  • UI components (prefer snapshot/interaction tests)
  • Database migrations
  • Third-party integrations

TDD Benefits

  • Design pressure: Hard-to-test code often has poor design
  • Documentation: Tests describe expected behavior
  • Confidence: Refactor without fear
  • Coverage: By definition, all code has tests

The discipline of TDD pays off most in long-lived codebases with many contributors.