用 AI 写单元测试的正确方法:从提升覆盖率到保证业务逻辑
用 AI 写单元测试的正确方法:从提升覆盖率到保证业务逻辑
很多团队引入 AI 写单元测试的第一个动作是:把函数贴给 AI,让它”补全测试”。结果覆盖率从 40% 飙到 85%,但下次上线还是出了 Bug。
覆盖率高不等于业务逻辑被覆盖。 这篇文章要解决的,正是这个核心矛盾。
为什么 AI 生成的单元测试容易”虚高”
AI 在没有上下文的情况下生成测试,会倾向于:
- 只测试 happy path,忽略边界条件
- 用
expect(fn()).toBeDefined()这类断言”凑数” - 对复杂业务逻辑做出错误假设,测试本身就是错的
- Mock 过度,把真正需要集成验证的路径全部绕开
根本原因在于:AI 不了解你的业务规则,它只能从代码结构推断”什么是合理的输入输出”。
正确姿势:先喂业务规则,再让 AI 写测试
提示词工程:三段式结构
与其把函数直接扔给 AI,不如用结构化 Prompt 给出足够的上下文:
## 角色
你是一位熟悉 [业务领域] 的高级前端工程师,擅长用 Vitest 编写高质量单元测试。
## 被测函数
[粘贴函数代码]
## 业务规则(重要)
1. [规则一,例如:折扣最大不能超过原价的 50%]
2. [规则二,例如:VIP 用户享受额外 9 折]
3. [规则三,例如:优惠券与折扣不可叠加]
## 要求
- 使用 Vitest,describe/it 结构
- 覆盖:正常流程、边界值、异常输入、业务规则冲突场景
- 每个 it 的描述用中文,清晰说明测试意图
- 不要使用 toBeDefined(),断言要具体到返回值
这三段中,”业务规则”是关键。 没有它,AI 只能猜。
完整示例:价格计算函数的测试
被测函数:
// utils/pricing.ts
export interface PricingInput {
originalPrice: number;
discountRate: number; // 0~1
isVip: boolean;
couponAmount: number;
}
export function calculateFinalPrice(input: PricingInput): number {
const { originalPrice, discountRate, isVip, couponAmount } = input;
if (originalPrice <= 0) throw new Error('原价必须大于 0');
if (discountRate < 0 || discountRate > 1) throw new Error('折扣率必须在 0~1 之间');
// 业务规则:折扣最大不超过原价 50%
const effectiveRate = Math.max(discountRate, 0.5);
let price = originalPrice * effectiveRate;
// 业务规则:VIP 额外 9 折,但优惠券与折扣不可叠加
if (isVip && couponAmount === 0) {
price = price * 0.9;
} else if (couponAmount > 0) {
price = price - couponAmount;
}
return Math.max(price, 0);
}
AI 根据业务规则生成的测试(使用 Vitest):
// utils/pricing.test.ts
import { describe, it, expect } from 'vitest';
import { calculateFinalPrice } from './pricing';
describe('calculateFinalPrice 价格计算', () => {
describe('正常流程', () => {
it('普通用户无优惠券:按折扣率计算', () => {
const result = calculateFinalPrice({
originalPrice: 100,
discountRate: 0.8,
isVip: false,
couponAmount: 0,
});
expect(result).toBe(80);
});
it('VIP 用户无优惠券:折扣后再享 9 折', () => {
const result = calculateFinalPrice({
originalPrice: 100,
discountRate: 0.8,
isVip: true,
couponAmount: 0,
});
expect(result).toBe(72); // 100 * 0.8 * 0.9
});
});
describe('业务规则:折扣上限 50%', () => {
it('折扣率低于 0.5 时,强制使用 0.5', () => {
const result = calculateFinalPrice({
originalPrice: 100,
discountRate: 0.2,
isVip: false,
couponAmount: 0,
});
expect(result).toBe(50); // 不能低于原价 50%
});
});
describe('业务规则:优惠券与折扣不可叠加', () => {
it('有优惠券时,VIP 折扣不生效', () => {
const result = calculateFinalPrice({
originalPrice: 100,
discountRate: 0.8,
isVip: true,
couponAmount: 10,
});
expect(result).toBe(70); // 80 - 10,VIP 9 折不叠加
});
});
describe('边界值与异常', () => {
it('优惠券金额超过折后价时,最终价格为 0(不为负)', () => {
const result = calculateFinalPrice({
originalPrice: 10,
discountRate: 0.8,
isVip: false,
couponAmount: 100,
});
expect(result).toBe(0);
});
it('原价为 0 时抛出异常', () => {
expect(() =>
calculateFinalPrice({ originalPrice: 0, discountRate: 0.8, isVip: false, couponAmount: 0 })
).toThrow('原价必须大于 0');
});
it('折扣率超出范围时抛出异常', () => {
expect(() =>
calculateFinalPrice({ originalPrice: 100, discountRate: 1.5, isVip: false, couponAmount: 0 })
).toThrow('折扣率必须在 0~1 之间');
});
});
});
AI 生成代码的审查清单
拿到 AI 的测试后,不要直接合入,过一遍这个清单:
断言质量检查
- 所有断言都有具体预期值,没有
toBeDefined()/toBeTruthy()凑数 - 异常测试用
toThrow()而不是try/catch包裹后空断言 - 浮点数比较使用
toBeCloseTo()而非严格相等
业务覆盖检查
- 每条业务规则至少有一个对应的
it测试 - 规则冲突场景(如本例中优惠券与 VIP 折扣)有专项测试
- 边界值:最小值、最大值、零值、负值都已覆盖
Mock 合理性检查
- 外部依赖(API、数据库)才 Mock,纯函数不应该有 Mock
- Mock 的返回值符合真实数据结构,不是随意构造的
{}
测试描述可读性
-
describe和it的文字组合起来,能读成一句完整的业务陈述 - 失败时的报错信息能快速定位是哪条业务规则被违反
进阶:让 AI 帮你发现测试盲区
在已有测试的基础上,用这个 Prompt 让 AI 做覆盖率分析:
以下是我的函数和现有测试,请分析:
1. 哪些代码分支没有被测试到(结合代码逻辑,不只是行覆盖)
2. 哪些业务场景可能在现实中出现,但测试中没有体现
3. 给出补充测试的建议(只列场景描述,不需要写代码)
[函数代码]
[现有测试代码]
这比直接看 Istanbul/V8 覆盖率报告更有价值——AI 能识别语义盲区,而不只是行未覆盖。
工具链推荐
| 场景 | 推荐工具 |
|---|---|
| 现代前端项目 | Vitest(与 Vite 生态无缝集成,速度快) |
| Node.js / 旧项目 | Jest |
| AI 辅助编写 | Claude Code、GitHub Copilot |
| 覆盖率可视化 | @vitest/coverage-v8 + lcov |
总结
用 AI 写单元测试的正确工作流:
- 整理业务规则,写成清单(这步不能省)
- 构造结构化 Prompt,把业务规则作为核心上下文
- 审查 AI 输出,对照清单逐项确认
- 让 AI 做盲区分析,补充语义层面的遗漏场景
- 把有效的 Prompt 模板沉淀为团队规范
覆盖率是结果,不是目标。真正的目标是:每一条业务规则,都有对应的测试在守护它。
延伸阅读
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 一个技术宅!
评论





