第 6 章:测试驱动开发 — 红灯绿灯重构

⏱ 预计阅读 10 分钟 更新于 2026/5/18
💡 进群学习加 wx: agentupdate
(申请发送: agentupdate)

你将学到什么

  • 什么是 TDD(测试驱动开发)以及为什么 AI 时代它更重要
  • 用 superpowers 的 /test-driven-development 工作流
  • 红灯 → 绿灯 → 重构的循环
  • 为 NoteFlow 补充边界测试用例

6.1 为什么 AI 时代测试更重要

有个反直觉的事实:AI 写代码越快,测试就越重要。

原因:

场景 没有测试 有测试
AI 写了一个函数 看起来对,但不知道边界情况对不对 跑一下测试就知道
改了一个功能 不知道有没有改坏其他功能 全量测试跑一遍,3 秒出结果
AI 重构了代码 不知道功能是否保持一致 测试通过 = 功能一致

测试是 AI 编码的安全网。 没有 safety net,你不敢让 AI 大规模改动代码。

6.2 TDD 三步循环

flowchart LR
    A["🔴 红灯
写一个失败的测试
(描述你想要的行为)"] --> B["🟢 绿灯
写最少的代码
让测试通过"] B --> C["♻️ 重构
改善代码结构
测试仍然通过"] C --> A style A fill:#fecaca,stroke:#dc2626 style B fill:#dcfce7,stroke:#16a34a style C fill:#dbeafe,stroke:#2563eb

红灯 — 先写测试,描述你想要的功能。这时候代码还没写,测试一定失败。

绿灯 — 写最少的代码让测试通过。不多不少,刚刚好。

重构 — 测试通过了,现在可以放心地改善代码。改完再跑一次测试,确认没改坏。

6.3 /test-driven-development 工作流

命令语法

/test-driven-development [功能描述]

实战:为 NoteFlow 补充搜索测试

第 5 章用 subagent 实现了核心功能,但搜索的边界情况测试不够。我们来补充。

/test-driven-development

为 NoteFlow 的搜索功能补充边界测试:
- 搜索空字符串应该返回所有笔记
- 搜索大小写应该不敏感
- 搜索结果应该高亮匹配文本
- 搜索特殊字符不应报错
- 搜索中文内容应正常工作

TDD 过程演示:

红灯:写测试

// src/hooks/useSearch.test.js
import { renderHook } from '@testing-library/react';
import { useSearch } from './useSearch';

describe('useSearch', () => {
  const notes = [
    { id: '1', title: '工作计划', content: '本周任务清单' },
    { id: '2', title: '购物清单', content: '牛奶、面包' },
    { id: '3', title: '学习笔记', content: 'React Hooks 总结' },
  ];

  test('空搜索返回所有笔记', () => {
    const { result } = renderHook(() => useSearch(notes, ''));
    expect(result.current).toHaveLength(3);
  });

  test('搜索不区分大小写', () => {
    const { result } = renderHook(() => useSearch(notes, 'react'));
    expect(result.current).toHaveLength(1);
    expect(result.current[0].id).toBe('3');
  });

  test('搜索特殊字符不报错', () => {
    const { result } = renderHook(() => useSearch(notes, '[test]'));
    expect(result.current).toHaveLength(0);
  });

  test('搜索中文内容', () => {
    const { result } = renderHook(() => useSearch(notes, '清单'));
    expect(result.current).toHaveLength(2);
  });
});

运行测试:

npm test -- src/hooks/useSearch.test.js

预期结果:红灯(函数还没实现)

绿灯:写最少的实现

// src/hooks/useSearch.js
import { useMemo } from 'react';

export function useSearch(notes, query) {
  return useMemo(() => {
    if (!query.trim()) return notes;

    const lowerQuery = query.toLowerCase();
    return notes.filter(note =>
      note.title.toLowerCase().includes(lowerQuery) ||
      note.content.toLowerCase().includes(lowerQuery)
    );
  }, [notes, query]);
}

运行测试:

npm test -- src/hooks/useSearch.test.js

预期结果:绿灯(4 tests passed)

重构:改善代码

当前实现可以工作,但有两个可以改进的地方:

  1. 搜索逻辑可以提取成独立函数,方便测试
  2. 可以加防抖,避免每次输入都触发搜索
// src/utils/search.js
export function filterNotes(notes, query) {
  if (!query.trim()) return notes;

  const lowerQuery = query.toLowerCase();
  return notes.filter(note =>
    note.title.toLowerCase().includes(lowerQuery) ||
    note.content.toLowerCase().includes(lowerQuery)
  );
}

// src/hooks/useSearch.js
import { useMemo } from 'react';
import { filterNotes } from '../utils/search';

export function useSearch(notes, query) {
  return useMemo(() => filterNotes(notes, query), [notes, query]);
}

运行测试:

npm test

绿灯(所有测试通过,重构没有改坏功能)。

6.4 边界测试清单

完整的 NoteFlow 测试应该覆盖以下边界情况:

mindmap
  root((测试覆盖))
    数据操作
      创建空标题笔记
      创建超长内容笔记
      删除不存在的笔记
      更新不存在的笔记
      localStorage 已满时的处理
    搜索
      空字符串搜索
      大小写不敏感
      特殊字符不报错
      中文搜索
      无结果时的空状态
    分类
      给笔记添加多个标签
      移除标签
      按标签筛选无结果
      空标签列表
    UI 交互
      笔记列表为空时的引导
      删除确认弹窗
      搜索输入框清空
      标签筛选切换

6.5 测试覆盖率目标

用 vitest 的覆盖率工具:

npm test -- --coverage

覆盖率等级:

等级 语句覆盖 适合场景
基础 > 60% 快速原型
合格 > 80% 正式项目(推荐)
优秀 > 95% 关键业务逻辑

对于 NoteFlow:

  • 工具函数(storage.js、search.js):目标 95%
  • 自定义 Hooks(useNotes.js、useSearch.js):目标 90%
  • UI 组件:目标 70%(UI 测试价值较低,不用追求高覆盖)

动手做

  1. 运行 /test-driven-development,为搜索功能补充测试
  2. 按照红灯 → 绿灯 → 重构的循环实现
  3. 运行 npm test -- --coverage 查看覆盖率
  4. 检查覆盖率报告,找出未覆盖的边界情况
  5. 补充缺失的测试

本章小结

概念 说明
红灯 先写失败的测试(描述期望行为)
绿灯 写最少代码让测试通过
重构 改善代码结构,测试仍然通过
边界测试 空值、特殊字符、异常输入等极端情况
覆盖率 量化测试完整性,80% 是合格线

核心原则: 测试先行,不是测试后补。先写测试能帮你想清楚"这个函数应该做什么",再写代码就是填空题而不是开放题。