零度AI
高级25 分钟阅读

Claude Code 测试驱动开发

使用 Claude Code 实践 TDD 开发模式,先写测试再写实现,红-绿-重构循环

Claude CodeTDD测试驱动开发红绿重构敏捷

TDD 概述

测试驱动开发(TDD)是一种先写测试再写实现的开发现模式。Claude Code 可以帮你实践 TDD 流程。

TDD 循环

红(Red)→ 绿(Green)→ 重构(Refactor):先写失败的测试,再写通过的实现,最后优化代码。

TDD 开发流程

需求分析

假设我们要实现一个待办事项列表的 CRUD 功能:

功能需求: 1. 创建待办事项(标题、描述、优先级、截止日期) 2. 标记待办事项为完成 3. 编辑待办事项 4. 删除待办事项 5. 列表查看待办事项(支持筛选和排序) 6. 批量操作(批量完成、批量删除)

第一步:编写失败的测试(红)

javascript
// src/test/tdd/todo-service.red.spec.js
import { describe, it, expect, beforeEach } from "vitest";
import { TodoService } from "../../../src/services/TodoService";

describe("TodoService - TDD 开发", () => {
  let todoService;

  beforeEach(() => {
    // TODO: 稍后实现
    todoService = new TodoService();
  });

  describe("创建待办事项", () => {
    it("应该能够创建一个基本的待办事项", () => {
      // Arrange
      const todoData = {
        title: "完成 TDD 教程",
        description: "学习测试驱动开发",
      };

      // Act
      const todo = todoService.createTodo(todoData);

      // Assert
      expect(todo).toBeDefined();
      expect(todo.id).toBeDefined();
      expect(todo.title).toBe("完成 TDD 教程");
      expect(todo.description).toBe("学习测试驱动开发");
      expect(todo.isCompleted).toBe(false);
      expect(todo.createdAt).toBeInstanceOf(Date);
    });

    it("标题为必填项", () => {
      const todoData = { description: "没有标题" };

      expect(() => todoService.createTodo(todoData)).toThrow("标题不能为空");
    });

    it("标题不能超过 200 字符", () => {
      const todoData = { title: "a".repeat(201) };

      expect(() => todoService.createTodo(todoData)).toThrow("标题不能超过 200 字符");
    });

    it("应该支持设置优先级", () => {
      const todoData = {
        title: "重要任务",
        priority: "high",
      };

      const todo = todoService.createTodo(todoData);

      expect(todo.priority).toBe("high");
    });

    it("应该支持设置截止日期", () => {
      const deadline = new Date("2026-12-31");
      const todoData = {
        title: "年底任务",
        deadline,
      };

      const todo = todoService.createTodo(todoData);

      expect(todo.deadline).toEqual(deadline);
    });
  });
});

第二步:编写通过的实现(绿)

javascript
// src/services/TodoService.js
export class TodoService {
  constructor() {
    this.todos = new Map();
    this.idCounter = 1;
  }

  createTodo({ title, description = "", priority = "medium", deadline = null }) {
    // 验证标题
    if (!title || title.trim() === "") {
      throw new Error("标题不能为空");
    }

    if (title.length > 200) {
      throw new Error("标题不能超过 200 字符");
    }

    const todo = {
      id: String(this.idCounter++),
      title: title.trim(),
      description: description.trim(),
      priority,
      deadline,
      isCompleted: false,
      createdAt: new Date(),
      updatedAt: new Date(),
    };

    this.todos.set(todo.id, todo);
    return todo;
  }
}

第三步:重构

javascript
// 重构后的版本 - 提取验证逻辑
// src/services/TodoService.js
export class TodoService {
  constructor() {
    this.todos = new Map();
    this.idCounter = 1;
  }

  createTodo({ title, description = "", priority = "medium", deadline = null }) {
    this.validateTitle(title);

    const todo = {
      id: String(this.idCounter++),
      title: title.trim(),
      description: description.trim(),
      priority: this.validatePriority(priority),
      deadline: this.validateDeadline(deadline),
      isCompleted: false,
      createdAt: new Date(),
      updatedAt: new Date(),
    };

    this.todos.set(todo.id, todo);
    return todo;
  }

  validateTitle(title) {
    if (!title || title.trim() === "") {
      throw new Error("标题不能为空");
    }
    if (title.length > 200) {
      throw new Error("标题不能超过 200 字符");
    }
  }

  validatePriority(priority) {
    const validPriorities = ["low", "medium", "high"];
    if (!validPriorities.includes(priority)) {
      throw new Error(`优先级必须是 ${validPriorities.join("、")} 之一`);
    }
    return priority;
  }

  validateDeadline(deadline) {
    if (deadline === null) return null;
    if (!(deadline instanceof Date) || isNaN(deadline.getTime())) {
      throw new Error("截止日期格式无效");
    }
    return deadline;
  }
}

继续 TDD 循环

标记完成功能

javascript
// src/test/tdd/todo-complete.red.spec.js
describe("标记完成", () => {
  let todoService;
  let createdTodo;

  beforeEach(() => {
    todoService = new TodoService();
    createdTodo = todoService.createTodo({
      title: "待完成的任务",
    });
  });

  it("应该能够标记待办事项为完成", () => {
    const result = todoService.markAsCompleted(createdTodo.id);

    expect(result.isCompleted).toBe(true);
    expect(result.completedAt).toBeInstanceOf(Date);
  });

  it("已完成的任务不应该重复标记", () => {
    todoService.markAsCompleted(createdTodo.id);

    expect(() => todoService.markAsCompleted(createdTodo.id)).toThrow(
      "该任务已经标记为完成"
    );
  });

  it("不存在的任务ID应该抛出错误", () => {
    expect(() => todoService.markAsCompleted("nonexistent-id")).toThrow(
      "任务不存在"
    );
  });

  it("应该能够取消完成状态", () => {
    todoService.markAsCompleted(createdTodo.id);

    const result = todoService.markAsIncomplete(createdTodo.id);

    expect(result.isCompleted).toBe(false);
    expect(result.completedAt).toBeUndefined();
  });
});
javascript
// 实现
export class TodoService {
  // ... 前面的代码 ...

  markAsCompleted(id) {
    const todo = this.todos.get(id);
    if (!todo) {
      throw new Error("任务不存在");
    }
    if (todo.isCompleted) {
      throw new Error("该任务已经标记为完成");
    }

    todo.isCompleted = true;
    todo.completedAt = new Date();
    todo.updatedAt = new Date();
    return todo;
  }

  markAsIncomplete(id) {
    const todo = this.todos.get(id);
    if (!todo) {
      throw new Error("任务不存在");
    }

    todo.isCompleted = false;
    todo.completedAt = undefined;
    todo.updatedAt = new Date();
    return todo;
  }
}

列表查询功能

测试用例

javascript
// src/test/tdd/todo-list.red.spec.js
describe("列表查询", () => {
  let todoService;

  beforeEach(() => {
    todoService = new TodoService();
    // 创建多个测试数据
    for (let i = 1; i <= 10; i++) {
      todoService.createTodo({
        title: `任务 ${i}`,
        priority: i % 3 === 0 ? "high" : i % 2 === 0 ? "medium" : "low",
      });
    }
  });

  it("应该返回所有待办事项", () => {
    const todos = todoService.getAllTodos();

    expect(todos).toHaveLength(10);
  });

  it("应该能够按优先级筛选", () => {
    const highPriorityTodos = todoService.getTodosByPriority("high");

    expect(highPriorityTodos.every((t) => t.priority === "high")).toBe(true);
    expect(highPriorityTodos).toHaveLength(3);
  });

  it("应该能够只获取未完成的任务", () => {
    // 标记前两个为完成
    const todos = todoService.getAllTodos();
    todoService.markAsCompleted(todos[0].id);
    todoService.markAsCompleted(todos[1].id);

    const pendingTodos = todoService.getPendingTodos();

    expect(pendingTodos).toHaveLength(8);
    expect(pendingTodos.every((t) => !t.isCompleted)).toBe(true);
  });

  it("应该能够按创建时间排序", () => {
    const todos = todoService.getTodosSortedBy("createdAt", "desc");

    expect(todos[0].title).toBe("任务 10");
    expect(todos[9].title).toBe("任务 1");
  });

  it("应该能够按优先级排序", () => {
    const todos = todoService.getTodosSortedBy("priority", "desc");

    // high 应该在最前面
    const highIndex = todos.findIndex((t) => t.priority === "high");
    expect(todos.slice(0, 3).every((t) => t.priority === "high")).toBe(true);
  });

  it("应该支持分页", () => {
    const page1 = todoService.getTodosPaginated({ page: 1, limit: 3 });
    const page2 = todoService.getTodosPaginated({ page: 2, limit: 3 });

    expect(page1.items).toHaveLength(3);
    expect(page2.items).toHaveLength(3);
    expect(page1.totalPages).toBe(4);
    expect(page2.totalPages).toBe(4);
  });
});

实现

javascript
// src/services/TodoService.js
export class TodoService {
  // ... 前面的代码 ...

  getAllTodos() {
    return Array.from(this.todos.values());
  }

  getTodosByPriority(priority) {
    return this.getAllTodos().filter((t) => t.priority === priority);
  }

  getPendingTodos() {
    return this.getAllTodos().filter((t) => !t.isCompleted);
  }

  getTodosSortedBy(field, order = "asc") {
    const todos = this.getAllTodos();
    const sortOrder = order === "desc" ? -1 : 1;

    return todos.sort((a, b) => {
      if (a[field] < b[field]) return -1 * sortOrder;
      if (a[field] > b[field]) return 1 * sortOrder;
      return 0;
    });
  }

  getTodosPaginated({ page = 1, limit = 10 }) {
    const todos = this.getAllTodos();
    const totalItems = todos.length;
    const totalPages = Math.ceil(totalItems / limit);
    const offset = (page - 1) * limit;

    return {
      items: todos.slice(offset, offset + limit),
      page,
      limit,
      totalItems,
      totalPages,
    };
  }
}

批量操作

测试

javascript
// src/test/tdd/todo-batch.red.spec.js
describe("批量操作", () => {
  let todoService;
  let createdTodos;

  beforeEach(() => {
    todoService = new TodoService();
    createdTodos = [];
    for (let i = 0; i < 5; i++) {
      createdTodos.push(
        todoService.createTodo({ title: `任务 ${i + 1}` })
      );
    }
  });

  it("应该能够批量标记为完成", () => {
    const ids = createdTodos.slice(0, 3).map((t) => t.id);

    const result = todoService.batchMarkAsCompleted(ids);

    expect(result.success).toHaveLength(3);
    expect(result.failed).toHaveLength(0);

    ids.forEach((id) => {
      expect(todoService.getTodoById(id).isCompleted).toBe(true);
    });
  });

  it("应该处理部分失败的批量操作", () => {
    const ids = [
      createdTodos[0].id,
      "nonexistent-id",
      createdTodos[1].id,
    ];

    const result = todoService.batchMarkAsCompleted(ids);

    expect(result.success).toHaveLength(2);
    expect(result.failed).toHaveLength(1);
    expect(result.failed[0].id).toBe("nonexistent-id");
  });

  it("应该能够批量删除", () => {
    const ids = createdTodos.slice(0, 3).map((t) => t.id);

    const result = todoService.batchDelete(ids);

    expect(result.success).toHaveLength(3);
    expect(todoService.getAllTodos()).toHaveLength(2);
  });
});

实现

javascript
// src/services/TodoService.js
export class TodoService {
  // ... 前面的代码 ...

  batchMarkAsCompleted(ids) {
    const result = { success: [], failed: [] };

    for (const id of ids) {
      try {
        this.markAsCompleted(id);
        result.success.push(id);
      } catch (error) {
        result.failed.push({ id, error: error.message });
      }
    }

    return result;
  }

  batchDelete(ids) {
    const result = { success: [], failed: [] };

    for (const id of ids) {
      try {
        this.deleteTodo(id);
        result.success.push(id);
      } catch (error) {
        result.failed.push({ id, error: error.message });
      }
    }

    return result;
  }
}

TDD 实践技巧

1. 小步前进

javascript
// 好的 TDD 实践:每次只做一个小的改动
// 先写一个失败的测试
it("应该返回空列表", () => {
  expect(todoService.getAllTodos()).toEqual([]);
});

// 写最简单的通过实现
getAllTodos() {
  return [];
}

2. 测试命名

javascript
// 清晰的测试命名
describe("TodoService", () => {
  describe("createTodo", () => {
    it("标题为空时应该抛出错误", () => { });
    it("标题超过200字符时应该抛出错误", () => { });
  });

  describe("markAsCompleted", () => {
    it("应该将任务标记为已完成", () => { });
    it("已完成任务重复标记时应该抛出错误", () => { });
  });
});

3. 测试隔离

javascript
// 每个测试使用独立的数据
beforeEach(() => {
  todoService = new TodoService(); // 每次都创建新的实例
});

it("应该正确处理独立的任务列表", () => {
  // 这个测试不会受其他测试影响
});

TDD 好处

提高代码质量、减少调试时间、更好的设计、更安全的重构、活文档。

TDD vs 传统测试

| 方面 | TDD | 传统测试 | |------|-----|----------| | 测试时机 | 实现前 | 实现后 | | 关注点 | 驱动设计 | 验证功能 | | 重构信心 | 高 | 中 | | 初期投入 | 较高 | 较低 | | 长期维护 | 较易 | 较难 |

总结

使用 Claude Code 实践 TDD:

  • 遵循红-绿-重构循环
  • 先写失败的测试
  • 写最简单通过的实现
  • 持续重构优化代码
  • 保持测试快速和独立
  • TDD 是一种技能,需要持续练习