TDD 练习:修复计数器组件
场景描述
你接手了一个计数器 Svelte 组件,测试已写好但实现有 Bug。你需要使用 TDD 方法修复这些问题。
需求规格
- 计数器初始值为 0
- 点击 "+" 按钮,计数 +1
- 点击 "-" 按钮,计数 -1
- 点击 "重置" 按钮,计数归零
- 计数有边界:最小 -10,最大 10
初始代码
计数器组件
svelte
<!-- Counter.svelte -->
<script>
let count = 0;
function increment() {
count++;
}
function decrement() {
count--;
}
function reset() {
count = 0;
}
</script>
<div class="counter">
<span class="count">{count}</span>
<button on:click={decrement}>-</button>
<button on:click={increment}>+</button>
<button on:click={reset}>重置</button>
</div>测试文件
javascript
// Counter.test.js
import { render, fireEvent } from '@testing-library/svelte';
import Counter from './Counter.svelte';
describe('Counter', () => {
test('初始值应为 0', () => {
const { getByText } = render(Counter);
expect(getByText('0')).toBeInTheDocument();
});
test('点击 + 按钮应增加计数', async () => {
const { getByText } = render(Counter);
await fireEvent.click(getByText('+'));
expect(getByText('1')).toBeInTheDocument();
});
test('点击 - 按钮应减少计数', async () => {
const { getByText } = render(Counter);
await fireEvent.click(getByText('-'));
expect(getByText('-1')).toBeInTheDocument();
});
test('计数不应低于 -10', async () => {
const { getByText } = render(Counter);
// 连续点击 12 次
for (let i = 0; i < 12; i++) {
await fireEvent.click(getByText('-'));
}
expect(getByText('-10')).toBeInTheDocument();
});
test('计数不应高于 10', async () => {
const { getByText } = render(Counter);
for (let i = 0; i < 12; i++) {
await fireEvent.click(getByText('+'));
}
expect(getByText('10')).toBeInTheDocument();
});
test('重置按钮应将计数归零', async () => {
const { getByText } = render(Counter);
await fireEvent.click(getByText('+'));
await fireEvent.click(getByText('+'));
await fireEvent.click(getByText('重置'));
expect(getByText('0')).toBeInTheDocument();
});
});你的任务
- 运行测试,观察哪些测试失败
- 分析问题,理解为什么会失败
- 使用 TDD 修复:
- 先确认失败的测试(RED)
- 写最小代码使测试通过(GREEN)
- 重构代码(REFACTOR)
- 确保所有测试通过
TDD 流程指南
第一步:RED(观察失败)
bash
npm test观察输出:
- 哪些测试失败?
- 失败原因是什么?
预期结果:边界测试失败(计数超出 -10 和 10 的限制)
第二步:GREEN(使其通过)
遵循 TDD 原则:
- 只修改使测试通过所需的最小代码
- 不要过度设计
- 不要添加测试未要求的功能
第三步:REFACTOR(重构)
测试通过后:
- 代码是否清晰?
- 是否有重复?
- 是否符合项目规范?
提示
点击展开提示
提示 1:边界检查
测试要求计数有上下限:
- 最小值:-10
- 最大值:10
当前代码没有任何边界检查。
提示 2:increment 函数
javascript
function increment() {
if (count < 10) { // 添加上限检查
count++;
}
}提示 3:decrement 函数
javascript
function decrement() {
if (count > -10) { // 添加下限检查
count--;
}
}提示 4:不要修改测试
TDD 的核心原则:测试是规格说明。如果测试正确,修改代码而非测试。
参考答案
点击展开参考答案
svelte
<!-- Counter.svelte -->
<script>
let count = 0;
const MIN = -10;
const MAX = 10;
function increment() {
if (count < MAX) {
count++;
}
}
function decrement() {
if (count > MIN) {
count--;
}
}
function reset() {
count = 0;
}
</script>
<div class="counter">
<span class="count">{count}</span>
<button on:click={decrement}>-</button>
<button on:click={increment}>+</button>
<button on:click={reset}>重置</button>
</div>
<style>
.counter {
display: flex;
align-items: center;
gap: 0.5rem;
}
.count {
font-size: 1.5rem;
min-width: 2rem;
text-align: center;
}
button {
padding: 0.25rem 0.5rem;
}
</style>关键修改
- 添加常量:
MIN和MAX定义边界 - 边界检查:
increment和decrement函数检查边界 - 保持简洁:只添加测试要求的功能,不过度设计
学习要点
1. RED-GREEN-REFACTOR 循环
┌───────┐ ┌───────┐
│ RED │────►│ GREEN │
│(失败) │ │(通过) │
└───────┘ └───┬───┘
▲ │
│ ┌────▼────┐
│ │REFACTOR │
└────────│ (重构) │
└─────────┘2. 测试即规格
测试不只是验证工具,更是行为规格。通过阅读测试,你应该能理解组件的完整行为。
3. 最小修改原则
TDD 强调只写使测试通过的最小代码。这避免过度设计和不必要的复杂性。
4. 不要修改通过的测试
如果测试合理但代码不通过,修改代码而非测试。
常见错误
| 错误 | 正确做法 |
|---|---|
| 先写代码后补测试 | 先写测试,观察失败,再写代码 |
| 修改测试使其通过 | 修改代码使其满足测试 |
| 添加测试未要求的功能 | 只实现测试要求的行为 |
| 跳过 RED 阶段 | 必须看到测试失败才写代码 |
进阶练习
完成基础练习后,尝试:
- 添加步长:允许用户设置每次增减的步长值
- 显示状态:达到边界时显示警告信息
- 键盘支持:支持键盘上下键操作
相关技能
- test-driven-development - TDD 核心技能
- verification-before-completion - 完成前验证
- systematic-debugging - 系统化调试