用 FizzBuzz 学单元测试:验收实例、参数化测试与基础重构
FizzBuzz 看起来像一道入门题,却非常适合练习真实开发中的三件事:需求会变化、规则会冲突、纯逻辑需要被可重复验证。这篇只聚焦单元测试和基础职责分离;浏览器 UI、后端 API 与遗留代码重构分别放在后续实操中展开。
这份练习使用 Vite 项目和 Vitest。Vitest 需要单独安装,并不是 Node.js 自带测试框架。
前置条件
- Node.js 和包管理器可用。
- 准备一个新的 Vite JavaScript 或 TypeScript 项目。
- 已提交初始项目,方便比较每轮变化。
安装 Vitest:
pnpm add -D vitest在 package.json 增加:
{
"scripts": {
"test": "vitest run"
}
}第一轮:建立基础规则
需求:
- 3 的倍数输出 fizz。
- 5 的倍数输出 buzz。
- 同时满足时输出 fizz buzz。
- 其他数字返回原数字。
先把需求写成实例:
| 输入 | 输出 |
|---|---|
| 1 | 1 |
| 3 | fizz |
| 5 | buzz |
| 15 | fizz buzz |
要求 AI 先实现纯函数,不要把计算逻辑直接写进页面事件。
第二轮:写参数化测试
import { describe, expect, it } from "vitest"
import { fizzBuzz } from "./fizz-buzz"
describe("fizzBuzz", () => {
it.each([
[1, "1"],
[3, "fizz"],
[5, "buzz"],
[15, "fizz buzz"],
])("输入 %s 时返回 %s", (input, expected) => {
expect(fizzBuzz(input)).toBe(expected)
})
})运行:
pnpm test先看到测试通过,再进入下一轮。
第三轮:需求开始变化
新增规则:7 的倍数输出 with,多种倍数同时满足时保持 fizz buzz with 顺序。
不要先改实现,先补验收:7、21、35、105 分别应该输出什么?如果产品没有明确,就必须先提问。
把确认后的用例加入测试,先看到失败,再修改实现直到全部通过。
第四轮:加入高优先级包含规则
新增:数字中包含 3 时直接输出 base,优先级高于倍数规则。
关键实例包括:
- 13 → base
- 30 → base
- 33 → base
- 15 → fizz buzz
这一步会迫使我们明确“包含”和“倍数”的优先级,而不是让 AI 自己猜。
可以继续加入“包含 5 高于包含 7”等规则,但每次只增加一轮,并保留全部旧测试。
第五轮:把页面和逻辑分离
页面只负责:读取输入、调用 fizzBuzz、展示结果和错误提示。所有规则放在独立模块中。
如果当前代码把 HTML 操作、输入校验和规则判断写在同一个函数里,按下面顺序重构:
- 确认现有测试全部通过。
- 提取纯计算函数。
- 页面改为调用新函数。
- 再次运行单元测试。
- 浏览器手工验证一遍主流程。
测试保证行为,浏览器验证集成,两者缺一不可。
成功标准
- 每次规则变化前先增加验收实例。
- 测试能够先失败、实现后再通过。
- 参数化数据清晰,没有复制大量断言。
- 参数表中的每一行都能独立读懂,不用追踪循环状态。
- 计算逻辑不依赖 DOM,可以被其他页面调用。
- 页面输入非法值时有明确反馈。
- 重构前后所有已确认行为一致。
常见问题与处理
AI 一次实现了所有未来规则
回退并按轮次开发。练习目标是体验变化与回归,不是最快得到最终答案。
测试跟着错误实现写
测试输入输出必须来自验收标准。先写预期,再让 AI 实现,避免它为自己的代码生成“证明自己正确”的测试。
Vitest 命令找不到
确认依赖已写入 devDependencies,使用项目包管理器运行脚本,不要把它误认为 Node 内置命令。
单测通过但页面失败
检查输入字符串到数字的转换、事件绑定和页面状态;再进入 Playwright UI 实操补一条关键行为冒烟测试,不要把所有页面问题都塞进单元测试。
课后练习
完成至少四轮规则变化,每轮单独提交 Git。最后查看提交历史,确认每一次都包含需求实例、测试和实现,而不是只有一份不可追踪的最终代码。