用 AI 生成单元测试:从第一个测试用例开始

前一篇文章中,我们学习了一个很重要的开发习惯:

先让 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.jsonscripts 中增加测试命令:

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 做代码优化:哪些建议值得采纳》


✍坚持原创,求关注,点赞,收藏

相关推荐
AI发掘1 小时前
手写稿被AI检测误标58%?三款AI降重工具实测:只改疑似段,不动原创内容
人工智能·aigc
滨哥GPT1 小时前
Codex修改环境变量后项目还是报错怎么办?.env、配置加载与运行环境排查
docker·ai编程·环境变量·开发环境·codex·env
qy2016skq2 小时前
OpenClaw 源码解读——入门与破局9 双插件协同:Quota Guard 负责“停“,Model Router 负责“绕“
langchain·prompt·aigc·embedding·ai编程·llama·agi
成愈秀2 小时前
Supervlint:一个测量“人类监督者是否还具备接管能力“的工具
ai编程
deli0070072 小时前
汉诺塔益智小游戏:浏览器里说句话,码道 WebUI 一键生成+部署上线
前端·ai编程
pqpo2 小时前
Agent Team 的上下文工程设计:如何组织和共享上下文
agent·ai编程
zhangfeng11332 小时前
HiDevLab vCANNLab(昇腾两个云端WebIDE)安装codebuddy
人工智能·ai编程·算子开发
leeyi2 小时前
Agent 间 Transfer 交接:用户在不同 Agent 间无缝切换(第93篇-E79)
人工智能·aigc·agent
李剑一3 小时前
前端转AI要了解的技术,其他人不用看。前端架构基础之:让Js的计算运行在GPU上
前端·aigc·ai编程