
前一篇文章中,我们学习了一个很重要的开发习惯:
先让 AI 出方案,再让它写代码。
方案确定之后,还有一个问题不能忽略:
怎样证明写出来的代码确实符合需求?
很多初学者会通过手动点击页面来检查功能。
手动测试当然有用,但它也有一些不足:
- 每次修改代码后,都需要重新操作一遍。
- 容易漏掉边界条件。
- 很难快速重复大量场景。
- 代码改动后,不容易确认旧功能有没有被影响。
这时,单元测试就可以发挥作用。
单元测试并不是一开始就要写复杂的测试系统。对初学者来说,可以先从一个简单函数开始:
text
准备输入
↓
调用函数
↓
得到实际结果
↓
和预期结果比较
AI 很适合帮助我们:
- 根据需求列出测试场景。
- 为函数生成测试代码。
- 补充容易遗漏的边界条件。
- 分析测试失败原因。
- 根据修改后的代码更新测试。
但测试是否真正覆盖了需求,仍然需要开发者自己判断。
一、什么是单元测试
"单元"可以理解为程序中一个相对独立、可以单独验证的最小功能。
例如:
- 一个计算价格的函数。
- 一个验证邮箱的函数。
- 一个格式化日期的函数。
- 一个根据状态返回文字的函数。
- 一个把数组转换成另一种结构的函数。
单元测试就是针对这些独立功能编写测试代码,确认它在不同输入下是否返回预期结果。
例如,有一个计算两数之和的函数:
javascript
function add(a, b) {
return a + b;
}
我们可以测试:
text
输入:2 和 3
预期结果:5
实际结果:调用 add(2, 3) 得到的结果
如果实际结果等于预期结果,测试通过;否则测试失败。
二、单元测试最核心的 3 个部分
初学者先记住下面三个概念就够了:
text
准备
↓
执行
↓
断言
1. 准备测试数据
准备函数需要的输入。
例如:
javascript
const price = 100;
const discount = 0.8;
2. 执行被测试的代码
调用需要验证的函数:
javascript
const result = calculatePrice(price, discount);
3. 断言结果
判断实际结果是否符合预期:
javascript
expect(result).toBe(80);
断言就是在告诉测试工具:
我希望这个结果必须是 80。
如果结果不是 80,测试就应该失败。
三、为什么要让 AI 帮助生成单元测试
单元测试最耗时的部分,通常不是写一条断言,而是思考:
- 哪些输入需要测试。
- 哪些情况容易出错。
- 边界值应该取什么。
- 异常情况是否需要验证。
- 多个条件组合后会不会出现问题。
AI 在整理测试场景方面非常有帮助。
例如,我们有一个计算折扣价格的函数:
javascript
function calculatePrice(price, discount) {
return price * discount;
}
可以先让 AI 列出测试场景:
text
请根据下面的 JavaScript 函数设计单元测试场景。
要求:
1. 列出正常输入。
2. 列出边界输入。
3. 列出异常输入。
4. 说明每个测试场景想验证什么。
5. 先不要写测试代码。
函数:
function calculatePrice(price, discount) {
return price * discount;
}
AI 可能会提醒我们考虑:
- 正常价格和正常折扣。
- 价格为 0。
- 折扣为 0。
- 折扣为 1。
- 价格为负数。
- 折扣大于 1。
- 参数不是数字。
但这里还有一个重要问题:
函数到底应该怎样处理这些异常输入?
当前代码没有做参数校验。
因此,我们不能直接把 AI 列出的所有场景都写成"必须通过"的测试,而要先确认业务规则。
四、先明确规则,再生成测试
测试不是凭空产生的。
它应该来自需求和业务规则。
例如,关于折扣价格,我们可以先确定:
text
业务规则:
1. price 必须是大于等于 0 的数字。
2. discount 必须是 0 到 1 之间的数字。
3. 参数不符合要求时抛出 Error。
4. 结果保留两位小数。
有了规则之后,AI 才能生成更准确的测试。
如果不提供规则,AI 可能做出不同假设:
- 把折扣
0.8理解成八折。 - 把折扣
80理解成八折。 - 允许负数价格。
- 遇到错误输入返回 0。
- 遇到错误输入返回
null。 - 遇到错误输入抛出异常。
这些做法没有绝对的统一答案,必须结合你的需求确认。
五、完整案例:为一个价格函数生成测试
下面我们使用一个包含校验逻辑的函数:
javascript
export function calculatePrice(price, discount) {
if (typeof price !== "number" || price < 0) {
throw new Error("price 必须是大于等于 0 的数字");
}
if (
typeof discount !== "number" ||
discount < 0 ||
discount > 1
) {
throw new Error("discount 必须是 0 到 1 之间的数字");
}
return Number((price * discount).toFixed(2));
}
这段代码的需求可以概括为:
- 正确计算价格。
- 允许价格为 0。
- 允许折扣为 0 和 1。
- 拒绝负数价格。
- 拒绝超出范围的折扣。
- 拒绝非数字参数。
- 最终结果保留两位小数。
第一步:先让 AI 列测试场景
可以这样提问:
text
请为下面的 calculatePrice 函数设计单元测试场景。
业务规则:
1. price 必须是大于等于 0 的数字。
2. discount 必须是 0 到 1 之间的数字。
3. 参数不符合要求时抛出 Error。
4. 结果保留两位小数。
请按照下面的分类输出:
1. 正常场景。
2. 边界场景。
3. 异常场景。
每个场景需要包含:
- 输入。
- 预期结果。
- 测试目的。
暂时不要生成测试代码。
第二步:检查 AI 列出的场景
可以整理成下面的测试清单:
| 类型 | 输入 | 预期结果 | 测试目的 |
|---|---|---|---|
| 正常 | 100, 0.8 |
80 |
验证普通折扣计算 |
| 正常 | 99.99, 0.9 |
89.99 |
验证小数计算和保留两位 |
| 边界 | 0, 0.8 |
0 |
验证最低价格 |
| 边界 | 100, 0 |
0 |
验证最低折扣 |
| 边界 | 100, 1 |
100 |
验证最高折扣 |
| 异常 | -1, 0.8 |
抛出错误 | 拒绝负数价格 |
| 异常 | 100, -0.1 |
抛出错误 | 拒绝负数折扣 |
| 异常 | 100, 1.1 |
抛出错误 | 拒绝超过 1 的折扣 |
| 异常 | "100", 0.8 |
抛出错误 | 拒绝字符串价格 |
| 异常 | 100, "0.8" |
抛出错误 | 拒绝字符串折扣 |
测试清单确认无误后,再让 AI 生成测试代码。
六、使用 Vitest 编写第一个单元测试
为了让案例完整,我们使用 Vitest 演示。
如果你的项目使用其他测试工具,核心思想基本相同,主要区别是配置和断言方法的写法。
1. 安装测试工具
在项目根目录执行:
bash
npm install -D vitest
然后在 package.json 的 scripts 中增加测试命令:
json
{
"scripts": {
"test": "vitest run"
}
}
如果项目原来已经有 test 命令,不要直接覆盖,应该结合当前项目的脚本配置进行调整。
2. 创建被测试文件
例如创建 src/calculate-price.js:
javascript
export function calculatePrice(price, discount) {
if (typeof price !== "number" || price < 0) {
throw new Error("price 必须是大于等于 0 的数字");
}
if (
typeof discount !== "number" ||
discount < 0 ||
discount > 1
) {
throw new Error("discount 必须是 0 到 1 之间的数字");
}
return Number((price * discount).toFixed(2));
}
3. 创建第一个测试文件
测试文件一般会和被测试文件放在相近的位置。
例如创建 src/calculate-price.test.js:
javascript
import { describe, expect, it } from "vitest";
import { calculatePrice } from "./calculate-price.js";
describe("calculatePrice", () => {
it("正常计算八折价格", () => {
const result = calculatePrice(100, 0.8);
expect(result).toBe(80);
});
});
这段代码可以分成几部分理解:
describe:把同一组测试放在一起。it:描述一个具体测试场景。calculatePrice(100, 0.8):执行被测试函数。expect(result).toBe(80):判断实际结果是否等于 80。
4. 执行测试
在项目根目录执行:
bash
npm test
如果测试通过,通常会看到通过数量和执行结果。
如果测试失败,测试工具会显示:
- 哪个测试失败。
- 预期结果是什么。
- 实际结果是什么。
- 失败发生在哪一行。
测试失败并不一定说明测试代码错了,也可能说明被测试的代码不符合需求。
七、让 AI 生成完整测试代码
第一个测试通过后,可以让 AI 根据测试清单补充其他场景。
推荐 Prompt:
text
请为下面的 calculatePrice 函数生成 Vitest 单元测试。
测试工具:
- Vitest
- JavaScript ES Module
业务规则:
1. price 必须是大于等于 0 的数字。
2. discount 必须是 0 到 1 之间的数字。
3. 参数不符合要求时抛出 Error。
4. 结果保留两位小数。
必须覆盖:
1. 正常价格计算。
2. 小数结果保留两位。
3. price 为 0。
4. discount 为 0。
5. discount 为 1。
6. price 为负数。
7. discount 小于 0。
8. discount 大于 1。
9. price 不是数字。
10. discount 不是数字。
要求:
1. 使用 describe 和 it。
2. 每个测试只验证一个明确行为。
3. 测试名称使用中文,能够说明测试目的。
4. 对抛出异常的场景使用正确的断言方式。
5. 不要修改被测试函数。
6. 生成后说明每组测试覆盖了什么。
AI 生成后,我们仍然要逐条阅读,而不是直接复制。
一个合理的测试文件可能是:
javascript
import { describe, expect, it } from "vitest";
import { calculatePrice } from "./calculate-price.js";
describe("calculatePrice", () => {
it("正常计算八折价格", () => {
expect(calculatePrice(100, 0.8)).toBe(80);
});
it("计算结果保留两位小数", () => {
expect(calculatePrice(99.99, 0.9)).toBe(89.99);
});
it("价格为 0 时返回 0", () => {
expect(calculatePrice(0, 0.8)).toBe(0);
});
it("折扣为 0 时返回 0", () => {
expect(calculatePrice(100, 0)).toBe(0);
});
it("折扣为 1 时返回原价", () => {
expect(calculatePrice(100, 1)).toBe(100);
});
it("价格为负数时抛出错误", () => {
expect(() => calculatePrice(-1, 0.8)).toThrow(
"price 必须是大于等于 0 的数字"
);
});
it("折扣小于 0 时抛出错误", () => {
expect(() => calculatePrice(100, -0.1)).toThrow(
"discount 必须是 0 到 1 之间的数字"
);
});
it("折扣大于 1 时抛出错误", () => {
expect(() => calculatePrice(100, 1.1)).toThrow(
"discount 必须是 0 到 1 之间的数字"
);
});
it("价格不是数字时抛出错误", () => {
expect(() => calculatePrice("100", 0.8)).toThrow(
"price 必须是大于等于 0 的数字"
);
});
it("折扣不是数字时抛出错误", () => {
expect(() => calculatePrice(100, "0.8")).toThrow(
"discount 必须是 0 到 1 之间的数字"
);
});
});
八、怎样检查 AI 生成的测试有没有价值
测试文件能运行,不代表测试写得好。
可以从下面几个方面检查。
1. 测试是否来自真实需求
如果需求是:
空输入不能新增待办。
测试就应该验证空输入时的行为,而不是只测试正常输入。
测试用例不是为了追求数量,而是为了验证功能规则。
2. 测试名称是否说明行为
不推荐:
javascript
it("test 1", () => {
// ...
});
推荐:
javascript
it("输入为空时不新增待办", () => {
// ...
});
看到测试名称,就应该大致知道它在保护什么行为。
3. 测试是否真的包含断言
下面这段测试表面上会运行,但没有验证任何结果:
javascript
it("调用计算函数", () => {
calculatePrice(100, 0.8);
});
它只是调用了函数,没有判断结果是否正确。
应该增加断言:
javascript
it("正常计算八折价格", () => {
const result = calculatePrice(100, 0.8);
expect(result).toBe(80);
});
4. 断言是否过于宽松
例如:
javascript
expect(result).toBeTruthy();
这个断言只能说明结果是真值,不能确认结果是不是正确的价格。
更准确的断言应该是:
javascript
expect(result).toBe(80);
断言越贴近需求,测试越有价值。
5. 测试是否彼此独立
一个测试不应该依赖另一个测试先执行。
不推荐让前一个测试修改全局数据,后一个测试再依赖修改后的结果。
每个测试都应该准备自己的输入,执行自己的操作,并验证自己的结果。
6. 测试是否覆盖了失败行为
很多 AI 生成的测试只覆盖"成功返回",却没有测试:
- 参数为空。
- 参数类型错误。
- 数据不存在。
- 接口失败。
- 权限不足。
- 网络超时。
测试失败行为同样重要,因为真实问题通常发生在非正常流程中。
九、让 AI 帮你补充边界测试
当正常测试已经完成后,可以单独让 AI 关注边界:
text
下面是已经存在的单元测试。
请只检查边界和异常场景是否遗漏,不要修改现有测试。
需要重点检查:
1. 空值。
2. 最小值和最大值。
3. 类型错误。
4. 数组为空。
5. 数组只有一项。
6. 重复数据。
7. 异步请求失败。
请输出:
1. 已覆盖的边界场景。
2. 未覆盖但建议补充的场景。
3. 每个新增场景的输入和预期结果。
这种提问比直接说"帮我完善测试"更容易得到聚焦的结果。
十、测试失败时,怎样让 AI 帮你分析
测试失败后,不要只发一句:
text
测试没通过,怎么办?
应该提供完整信息:
text
我的 Vitest 测试失败了,请帮我分析原因。
测试名称:
正常计算八折价格
完整失败信息:
[粘贴测试工具输出]
被测试函数:
[粘贴相关代码]
测试代码:
[粘贴失败的测试]
预期结果:
80
实际结果:
79.99
请按以下顺序回答:
1. 解释失败信息。
2. 判断更可能是业务规则、实现代码还是测试代码的问题。
3. 给出最小排查步骤。
4. 不要直接修改代码,先说明判断依据。
AI 可以帮助分析,但最终要结合业务规则确认。
例如价格计算中的小数问题,可能涉及:
- 是否要求四舍五入。
- 是否要求截断。
- 是否应该使用整数分保存金额。
- 浮点数计算是否会带来误差。
涉及金额时,不能只看测试"通过"就认为实现一定安全。
十一、常见的单元测试误区
误区一:只测试正常输入
正常输入只能证明最顺利的路径可以运行。
还需要测试边界和异常,才能发现参数校验、空值处理和错误提示的问题。
误区二:测试代码和实现代码完全一样
如果测试只是重复实现代码中的计算过程,可能会把同一个错误复制一遍。
测试应该表达需求和预期,而不是把被测代码重新写一次。
误区三:为了提高覆盖率而堆测试数量
覆盖率高不等于测试质量高。
大量重复测试可能只是在测试相同路径,没有增加实际保护。
更重要的是覆盖关键业务规则和高风险场景。
误区四:测试失败就修改测试
测试失败时,不要为了让命令变绿就直接改预期结果。
先确认:
- 需求是否发生变化。
- 实现代码是否有问题。
- 测试输入是否合理。
- 预期结果是否符合业务规则。
误区五:让 AI 一次生成整个项目的测试
一次生成大量测试,可能带来:
- 测试依赖不存在的文件。
- 测试框架配置不匹配。
- 测试使用了项目没有的接口。
- 断言和真实业务不一致。
- 出错后很难判断是哪一组测试有问题。
更适合的方式是从一个函数、一组规则开始,逐步增加测试。
误区六:把单元测试当成全部测试
单元测试主要验证独立函数或模块。
它不能完全替代:
- 页面交互测试。
- 接口联调测试。
- 数据库测试。
- 端到端测试。
- 性能测试。
- 安全测试。
不同类型的测试负责发现不同问题。
十二、单元测试和其他测试有什么区别
初学者可以先用下面的方式理解:
| 测试类型 | 主要验证什么 | 示例 |
|---|---|---|
| 单元测试 | 一个函数或小模块 | 计算折扣是否正确 |
| 接口测试 | 接口输入和返回结果 | 搜索接口是否返回正确商品 |
| 组件测试 | 一个页面组件的行为 | 点击按钮后是否显示列表 |
| 端到端测试 | 用户完整操作流程 | 登录后进入首页 |
| 性能测试 | 系统在压力下的表现 | 大量请求时响应时间 |
AI 可以帮助设计这些测试,但第一步仍然建议从最小的单元测试开始。
十三、可直接复用的单元测试 Prompt 模板
下面是一份可以直接使用的模板:
text
请为下面的代码生成单元测试。
项目环境:
- 编程语言:[例如 JavaScript]
- 测试框架:[例如 Vitest]
- 模块格式:[例如 ES Module]
被测试代码:
[粘贴函数或模块代码]
业务需求:
[说明这段代码应该实现什么]
已知规则:
- [规则 1]
- [规则 2]
- [规则 3]
请先输出测试场景清单,不要立即写代码。
测试场景需要覆盖:
1. 正常输入。
2. 边界输入。
3. 异常输入。
4. 空值和类型错误。
5. 依赖失败或返回异常。
确认场景后,再生成测试代码。
代码要求:
1. 使用当前项目已有的测试框架。
2. 每个测试只验证一个明确行为。
3. 测试名称清楚描述预期行为。
4. 每个测试必须包含有效断言。
5. 不修改被测试代码。
6. 说明每组测试覆盖了哪些业务规则。
7. 如果信息不足,请先列出需要确认的问题。
如果已经有一组测试,还可以使用这个模板:
text
请审查下面的单元测试,不要直接重写。
请检查:
1. 是否覆盖业务需求。
2. 是否缺少边界和异常场景。
3. 断言是否准确。
4. 测试之间是否相互独立。
5. 是否存在重复或没有价值的测试。
6. 是否依赖了不稳定的时间、随机数或外部服务。
请先输出问题清单和修改建议,
再给出需要新增或修改的最小测试代码。
十四、一个适合新手的测试流程
以后为一个新函数写测试时,可以按照下面的流程:
text
明确业务规则
↓
让 AI 列出测试场景
↓
自己确认场景和预期
↓
让 AI 生成最小测试代码
↓
运行测试
↓
分析失败原因
↓
补充边界和异常测试
每一步都要有自己的判断。
不要一开始追求写出几十个测试,先保证最重要的规则被准确验证。
十五、总结
单元测试的入门并不复杂,核心就是:
text
给定输入
↓
调用代码
↓
验证预期结果
使用 AI 生成单元测试时,要注意:
- 先明确业务规则,再让 AI 设计测试。
- 先列测试场景,再生成测试代码。
- 正常、边界和异常情况都要考虑。
- 每个测试都应该包含清晰、准确的断言。
- 测试文件能运行,不代表测试真的有价值。
- 测试失败后,要结合需求判断是实现、测试还是规则出了问题。
- 单元测试不能替代接口、页面、性能和安全测试。
可以把今天的内容浓缩成一句话:
AI 可以帮你写测试代码,但你必须负责确认测试到底应该证明什么。
当你开始为代码补充测试时,AI 的作用就不只是生成实现代码,也能帮助你更早发现问题、保护已有功能。
下一篇文章,我们将继续学习:
《用 AI 做代码优化:哪些建议值得采纳》
✍坚持原创,求关注,点赞,收藏