Development
测试驱动开发
用测试来驱动开发。在实现任何逻辑、修复任何 Bug 或更改任何行为时使用。证明代码确实可用。
在编写让测试通过的代码之前,先编写一个会失败的测试。测试就是证明——"看起来没问题"不等于完成。
不适用场景: 纯配置变更、文档更新或静态内容变更。
RED GREEN REFACTOR
Write a test Write minimal code Clean up the
that fails ──→ to make it pass ──→ implementation
// RED: This test fails because createTask doesn't exist yet
describe('TaskService', () => {
it('creates a task with title and default status', async () => {
const task = await taskService.createTask({ title: 'Buy groceries' });
expect(task.id).toBeDefined();
expect(task.title).toBe('Buy groceries');
expect(task.status).toBe('pending');
expect(task.createdAt).toBeInstanceOf(Date);
});
});
// GREEN: Minimal implementation
export async function createTask(input: { title: string }): Promise<Task> {
const task = {
id: generateId(),
title: input.title,
status: 'pending' as const,
createdAt: new Date(),
};
await db.tasks.insert(task);
return task;
}
在测试通过的情况下,在不改变行为的前提下改进代码:
当收到 Bug 报告时,不要一开始就尝试去修复它。 先编写一个能复现该 Bug 的测试。
// Step 1: Write the reproduction test (it should FAIL)
it('sets completedAt when task is completed', async () => {
const task = await taskService.createTask({ title: 'Test' });
const completed = await taskService.completeTask(task.id);
expect(completed.status).toBe('completed');
expect(completed.completedAt).toBeInstanceOf(Date); // This fails → bug confirmed
});
// Step 2: Fix the bug
export async function completeTask(id: string): Promise<Task> {
return db.tasks.update(id, {
status: 'completed',
completedAt: new Date(), // This was missing
});
}
// Step 3: Test passes → bug fixed, regression guarded
按照金字塔来分配测试投入:
╱╲
╱ ╲ E2E Tests (~5%)
╱ ╲ Full user flows, real browser
╱──────╲
╱ ╲ Integration Tests (~15%)
╱ ╲ Component interactions, API boundaries
╱────────────╲
╱ ╲ Unit Tests (~80%)
╱ ╲ Pure logic, isolated, milliseconds each
╱──────────────────╲
| 规模 | 约束 | 速度 | 示例 | |------|------------|-------|---------| | 小型 | 单进程,无 I/O,无网络 | 毫秒级 | 纯函数测试 | | 中型 | 允许多进程,仅限 localhost | 秒级 | 使用测试数据库的 API 测试 | | 大型 | 允许多机器,外部服务 | 分钟级 | E2E 测试 |
// Good: Tests what the function does (state-based)
it('returns tasks sorted by creation date', async () => {
const tasks = await listTasks({ sortBy: 'createdAt', sortOrder: 'desc' });
expect(tasks[0].createdAt.getTime())
.toBeGreaterThan(tasks[1].createdAt.getTime());
});
// Bad: Tests how the function works internally
it('calls db.query with ORDER BY created_at DESC', async () => {
await listTasks({ sortBy: 'createdAt', sortOrder: 'desc' });
expect(db.query).toHaveBeenCalledWith(
expect.stringContaining('ORDER BY created_at DESC')
);
});
在测试中,DAMP(描述性且有意义的短语,Descriptive And Meaningful Phrases) 优于 DRY。每个测试都应该讲述一个完整的故事。
// DAMP: Each test is self-contained and readable
it('rejects tasks with empty titles', () => {
const input = { title: '', assignee: 'user-1' };
expect(() => createTask(input)).toThrow('Title is required');
});
it('trims whitespace from titles', () => {
const input = { title: ' Buy groceries ', assignee: 'user-1' };
const task = createTask(input);
expect(task.title).toBe('Buy groceries');
});
Preference order (most to least preferred):
1. Real implementation → Highest confidence
2. Fake → In-memory version
3. Stub → Returns canned data
4. Mock (interaction) → Verifies method calls — use sparingly
it('marks overdue tasks when deadline has passed', () => {
// Arrange: Set up the test scenario
const task = createTask({
title: 'Test',
deadline: new Date('2025-01-01'),
});
// Act: Perform the action being tested
const result = checkOverdue(task, new Date('2025-01-02'));
// Assert: Verify the outcome
expect(result.isOverdue).toBe(true);
});
| 反模式 | 问题 | 修复方法 | |---|---|---| | 测试实现细节 | 重构时测试会失败 | 测试输入与输出 | | 不稳定(Flaky)的测试 | 削弱对测试套件的信任 | 使用确定性断言 | | 测试框架代码 | 浪费时间 | 只测试你自己的代码 | | 滥用快照(Snapshot) | 没人审查的大型快照 | 谨慎使用并加以审查 | | 缺乏测试隔离 | 测试会一起失败 | 每个测试自行设置其状态 | | 对所有东西都 Mock | 测试通过但生产环境出问题 | 优先使用真实实现 |
在完成任何实现之后:
npm test