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 是一种技能,需要持续练习