Skip to content

TDD 练习:修复计数器组件

场景描述

你接手了一个计数器 Svelte 组件,测试已写好但实现有 Bug。你需要使用 TDD 方法修复这些问题。

需求规格

  1. 计数器初始值为 0
  2. 点击 "+" 按钮,计数 +1
  3. 点击 "-" 按钮,计数 -1
  4. 点击 "重置" 按钮,计数归零
  5. 计数有边界:最小 -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();
  });
});

你的任务

  1. 运行测试,观察哪些测试失败
  2. 分析问题,理解为什么会失败
  3. 使用 TDD 修复
    • 先确认失败的测试(RED)
    • 写最小代码使测试通过(GREEN)
    • 重构代码(REFACTOR)
  4. 确保所有测试通过

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>

关键修改

  1. 添加常量MINMAX 定义边界
  2. 边界检查incrementdecrement 函数检查边界
  3. 保持简洁:只添加测试要求的功能,不过度设计

学习要点

1. RED-GREEN-REFACTOR 循环

   ┌───────┐     ┌───────┐
   │ RED   │────►│ GREEN │
   │(失败) │     │(通过) │
   └───────┘     └───┬───┘
       ▲             │
       │        ┌────▼────┐
       │        │REFACTOR │
       └────────│ (重构)  │
                └─────────┘

2. 测试即规格

测试不只是验证工具,更是行为规格。通过阅读测试,你应该能理解组件的完整行为。

3. 最小修改原则

TDD 强调只写使测试通过的最小代码。这避免过度设计和不必要的复杂性。

4. 不要修改通过的测试

如果测试合理但代码不通过,修改代码而非测试。

常见错误

错误正确做法
先写代码后补测试先写测试,观察失败,再写代码
修改测试使其通过修改代码使其满足测试
添加测试未要求的功能只实现测试要求的行为
跳过 RED 阶段必须看到测试失败才写代码

进阶练习

完成基础练习后,尝试:

  1. 添加步长:允许用户设置每次增减的步长值
  2. 显示状态:达到边界时显示警告信息
  3. 键盘支持:支持键盘上下键操作

相关技能