使用 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 本身进行断言的测试,而不是针对行为进行断言。像这样的测试会给你虚假的信心:
test('创建一个用户', () => {
const user = createUser('Alice', 'alice@example.com')
expect(user).toBeDefined() // 这对几乎任何内容都会通过
})更好的断言是检查实际的属性:
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 不正确。看起来正确的测试在聊天窗口中可能没问题,但实际执行时可能会立即失败:
vitest run src/userService.test.js是否涵盖了真实的边缘情况?
AI 工具倾向于生成快乐路径测试并跳过困难的情况。审查生成的测试后,问问自己:如果输入为空怎么办?如果是 null 或 undefined 呢?如果网络请求失败怎么办?如果列表为空怎么办?
如果这些场景没有被覆盖,请让 AI 添加它们,或者自己编写。
迭代输出
将 AI 生成的测试视为初稿,而不是成品。良好的工作流程如下:
- 生成具有明确提示和良好上下文的初始测试
- 运行它们以立即捕获错误
- 审查每个测试是否存在上述问题
- 请求修订,如果整个部分需要改进(“这些测试 Mock 过多,请重写以测试与数据库模块的实际集成”)
- 手动编辑进行小修,而不是对每个细节重新提示
随着时间推移,当 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 run 或 vitest --no-watch 来确保进程在测试完成后退出。
