用 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 的返回值符合真实数据结构,不是随意构造的 {}

测试描述可读性

  • describeit 的文字组合起来,能读成一句完整的业务陈述
  • 失败时的报错信息能快速定位是哪条业务规则被违反

进阶:让 AI 帮你发现测试盲区

在已有测试的基础上,用这个 Prompt 让 AI 做覆盖率分析:

以下是我的函数和现有测试,请分析:
1. 哪些代码分支没有被测试到(结合代码逻辑,不只是行覆盖)
2. 哪些业务场景可能在现实中出现,但测试中没有体现
3. 给出补充测试的建议(只列场景描述,不需要写代码)

[函数代码]
[现有测试代码]

这比直接看 Istanbul/V8 覆盖率报告更有价值——AI 能识别语义盲区,而不只是行未覆盖。


工具链推荐

场景 推荐工具
现代前端项目 Vitest(与 Vite 生态无缝集成,速度快)
Node.js / 旧项目 Jest
AI 辅助编写 Claude Code、GitHub Copilot
覆盖率可视化 @vitest/coverage-v8 + lcov

总结

用 AI 写单元测试的正确工作流:

  1. 整理业务规则,写成清单(这步不能省)
  2. 构造结构化 Prompt,把业务规则作为核心上下文
  3. 审查 AI 输出,对照清单逐项确认
  4. 让 AI 做盲区分析,补充语义层面的遗漏场景
  5. 把有效的 Prompt 模板沉淀为团队规范

覆盖率是结果,不是目标。真正的目标是:每一条业务规则,都有对应的测试在守护它。


延伸阅读