Skip to content

使用 AI 编写测试

AI 编码助手可以帮助你更快地编写测试,但输出质量在很大程度上取决于你提供的上下文。模糊的提示会产生模糊的测试,而带有正确上下文的明确提示则能生成真正值得保留的测试。

本页面介绍了如何从 AI 工具中获得高质量的测试代码,以及在审查结果时需要注意的问题。

提供上下文

你能做的最重要的一件事就是给 AI 足够的上下文,让它理解正在测试什么。

从源文件本身开始。AI 需要看到实际的实现,而不仅仅是功能的描述。包含完整的文件,或者至少包含你想要测试的函数及其导入和类型。

分享同一项目的现有测试文件。这有助于 AI 匹配你的约定:你是使用 test 还是 it,如何组织 describe 块,是否更喜欢 test.extend 夹具还是 beforeEach,以及如何命名测试。AI 工具擅长模式匹配,但它们需要有可以匹配的模式。

包含你的 Vitest 配置,特别是如果你启用了 globals、设置了自定义 environment 或配置了 setupFiles。没有这些上下文,AI 可能会生成不必要的导入,使用错误的测试环境,或者遗漏测试依赖的设置。

如果被测代码有依赖项需要 Mock,也请分享这些文件(或至少提供它们的类型签名)。AI 无法为它从未见过的数据库客户端编写有用的 Mock。

TIP

如果你的项目中有一个包含编码约定的 AGENTS.md 或类似文件,也请一并包含。许多 AI 工具会自动识别这些文件并遵循其中定义的规则。

编写良好的提示

明确的提示比通用的提示能产生更好的测试。让我们比较一下这两个示例:

模糊的: “为 userService.js 编写测试”

这会生成测试,但它们可能很浅显:每个函数只有一个快乐路径测试,最小程度的边缘情况覆盖,并且测试名称通用。

更好的: “为 userService.js 中的 createUser 函数编写测试。覆盖验证错误(缺少名称、无效的电子邮件格式、重复的电子邮件)、成功创建路径,并验证密码在存储前已进行哈希处理。”

这告诉 AI 重点关注哪个函数,哪些场景是重要的,以及需要验证哪些行为。输出会更全面且更相关。

编写更好提示的技巧

  • 明确要求边缘情况。“包括针对空输入、边界值和错误处理的测试”会比让 AI 自己判断产生更全面的覆盖率。如果不给出这种提示,大多数工具只会生成几个快乐路径测试然后就停止。
  • 如果希望使用特定的 Vitest 功能,请提及。例如,“使用 toMatchInlineSnapshot 处理错误消息”或“使用 test.each 处理不同的货币格式”,可以引导 AI 使用正确的工具,而不是让它退回到重复的复制粘贴测试。
  • 如果你在测试异步代码,请明确说明。“函数返回一个 Promise”或“这调用了外部 API”有助于 AI 使用 async/await 以及适当的匹配器,如 .resolves.rejects
  • 告诉 AI 不要 做什么。“针对真实实现进行测试,不要 Mock 任何模块”或“不要使用快照测试”可以防止你不想要的常见默认行为。AI 工具倾向于过度 Mock,明确约束可以防止这种情况。
  • 描述你想要的测试结构。“使用 describe 块按方法分组测试”或“使用 test.extend 夹具代替 beforeEach 来处理数据库连接”可以省去你之后重组输出的麻烦。
  • 在请求新增内容时引用现有测试。“遵循 auth.test.js 中的测试风格”比从头描述风格更有效。AI 会从示例中识别命名约定、断言模式和导入风格。
  • 如果第一个结果不理想,请继续迭代。“这些测试过于关注实现细节。将它们重写为只断言返回值和抛出的错误”是一个有效的后续操作。通过对话进行细化通常比试图一次性写出完美的提示更好。

审查 AI 生成的测试

AI 生成的测试第一眼看起来很具有说服力,但仍可能存在问题。在提交之前,请检查以下几点。

测试是否真的在断言有意义的内容?

注意那些只调用函数但只检查是否不抛出异常的测试,或者只针对 Mock 本身进行断言的测试,而不是针对行为进行断言。像这样的测试会给你虚假的信心:

js
test('创建一个用户', () => {
  const user = createUser('Alice', 'alice@example.com')
  expect(user).toBeDefined() // 这对几乎任何内容都会通过
})

更好的断言是检查实际的属性:

js
test('创建具有正确字段的用户', () => {
  const user = createUser('Alice', 'alice@example.com')
  expect(user).toMatchObject({
    name: 'Alice',
    email: 'alice@example.com',
  })
  expect(user.id).toBeTypeOf('string')
})

是在测试行为还是实现?

AI 倾向于过度 Mock。如果你看到一个测试 Mock 了每个依赖项,然后断言内部方法是否以特定顺序被调用,这就是在测试实现细节。这些测试每次重构时都会断裂,即使行为保持不变。

问问自己:如果有人改变了内部实现但函数仍然返回正确的结果,这个测试会断裂吗?如果会,那它可能与实现耦合过紧。更多关于这种区别的内容,请参见实际测试

测试是否真的运行?

始终在提交前运行测试。AI 生成的测试可能存在导入错误、引用不存在的函数,或者使用 API 不正确。看起来正确的测试在聊天窗口中可能没问题,但实际执行时可能会立即失败:

bash
vitest run src/userService.test.js

是否涵盖了真实的边缘情况?

AI 工具倾向于生成快乐路径测试并跳过困难的情况。审查生成的测试后,问问自己:如果输入为空怎么办?如果是 nullundefined 呢?如果网络请求失败怎么办?如果列表为空怎么办?

如果这些场景没有被覆盖,请让 AI 添加它们,或者自己编写。

迭代输出

将 AI 生成的测试视为初稿,而不是成品。良好的工作流程如下:

  1. 生成具有明确提示和良好上下文的初始测试
  2. 运行它们以立即捕获错误
  3. 审查每个测试是否存在上述问题
  4. 请求修订,如果整个部分需要改进(“这些测试 Mock 过多,请重写以测试与数据库模块的实际集成”)
  5. 手动编辑进行小修,而不是对每个细节重新提示

随着时间推移,当 AI 看到更多代码库和测试模式时,它的输出会有所改进。早期测试会为后续所有测试设定模式,因此值得花时间做好这些。

常见陷阱

错误的 API

AI 生成的 Vitest 测试中最常见的问题是使用了错误的 API 表面。AI 模型在大量 Jest 代码上进行了训练,因此有时会生成 jest.fn() 而不是 vi.fn(),或者使用 jest.mock 而不是 vi.mock。这些会立即失败。

相关的问题是导入:如果你的配置启用了 globals: true,AI 可能会仍然添加 import { test, expect } from 'vitest'(虽然无害但没必要),或者反过来,在没有启用全局变量时生成没有导入的测试。如果你不断看到 Jest API,请将 Vitest API 参考 告知 AI,或将其包含在上下文中。

Mock 清理

AI 生成的测试经常设置 Spy(使用 vi.spyOn)或替换模块(使用 vi.mock),但从未恢复它们。如果你的配置没有启用 restoreMocks: true,这些 Mock 会在测试之间泄漏并导致令人困惑的失败。最简单的修复方法是全局启用该配置选项。

相关值得注意的是,AI 工具倾向于使用字符串路径(vi.mock('./module.js'))来 Mock 模块,而更推荐使用 import() 形式(vi.mock(import('./module.js'))),因为它在类型安全和自动重构方面更可靠。更多原因请参见Mock 函数

冗长的测试名称

AI 倾向于生成类似“当给定一个有效的正数和一个支持的货币代码时,正确返回格式化价格字符串”的测试名称。这些名称在拥有几十个测试时难以浏览。更短的名称能更好地描述行为,例如“格式化 USD 价格”、“对负数抛出异常”、“当没有匹配项时返回空数组”。

监听模式

Vitest 默认以监听模式运行,等待文件变化并交互式重新运行测试。Vitest 会尝试检测 CI 和非交互式或代理环境并自动禁用监听模式,但这种检测可能不稳定。

当告诉 AI 代理运行测试时,请始终使用 vitest runvitest --no-watch 来确保进程在测试完成后退出。