金句:写测试不是在怀疑自己的代码,而是在保护未来的自己不被今天的自己坑。AI 的出现让这件"应该做但总是拖着不做"的事情,终于变得轻松了。
一、为什么开发者不爱写测试?
在 Stack Overflow 的年度调查中,"测试覆盖率不足"连续五年进入开发者最大技术债务前三名。
原因很简单:
- 写测试比写功能无聊——没有即时的成就感
- 写测试耗时——有时测试代码比被测代码还多
- 边界情况难以穷举——总觉得还有情况没覆盖到
AI 辅助测试正在改变这三个问题。
二、AI 生成测试的四种模式
模式一:从代码生成测试(最常用)
给 AI 看你的代码,让它生成测试用例。
示例代码(被测函数):
// src/utils/price.ts
export function calculateDiscount(
originalPrice: number,
discountPercent: number,
memberLevel: 'normal' | 'vip' | 'svip'
): number {
if (originalPrice <= 0) {
throw new Error('价格必须大于0');
}
if (discountPercent < 0 || discountPercent > 100) {
throw new Error('折扣率必须在0-100之间');
}
const memberBonus = {
normal: 0,
vip: 5,
svip: 10
};
const totalDiscount = Math.min(discountPercent + memberBonus[memberLevel], 100);
return originalPrice * (1 - totalDiscount / 100);
}
提示词:
为以上 calculateDiscount 函数生成完整的 Vitest 测试用例。
要求:
1. 覆盖正常情况(所有会员等级)
2. 覆盖边界情况(折扣率为0、100,价格刚好为正)
3. 覆盖异常情况(所有会抛出异常的入参)
4. 折扣叠加的组合情况(VIP + 高折扣是否超过100%限制)
AI 生成的测试:
import { describe, it, expect } from 'vitest';
import { calculateDiscount } from '../src/utils/price';
describe('calculateDiscount', () => {
// 正常情况
describe('普通会员', () => {
it('应用 10% 折扣', () => {
expect(calculateDiscount(100, 10, 'normal')).toBe(90);
});
it('应用 50% 折扣', () => {
expect(calculateDiscount(200, 50, 'normal')).toBe(100);
});
});
describe('VIP 会员(额外 5% 优惠)', () => {
it('应用 10% 折扣 + VIP 加成', () => {
expect(calculateDiscount(100, 10, 'vip')).toBe(85);
});
it('折扣叠加不超过 100%', () => {
expect(calculateDiscount(100, 96, 'vip')).toBe(0);
});
});
describe('SVIP 会员(额外 10% 优惠)', () => {
it('应用 20% 折扣 + SVIP 加成', () => {
expect(calculateDiscount(100, 20, 'svip')).toBe(70);
});
it('折扣叠加不超过 100%', () => {
expect(calculateDiscount(100, 91, 'svip')).toBe(0);
});
});
// 边界情况
describe('边界情况', () => {
it('折扣率为 0', () => {
expect(calculateDiscount(100, 0, 'normal')).toBe(100);
});
it('折扣率为 100', () => {
expect(calculateDiscount(100, 100, 'normal')).toBe(0);
});
it('价格为最小正数', () => {
expect(calculateDiscount(0.01, 0, 'normal')).toBeCloseTo(0.01);
});
});
// 异常情况
describe('异常输入', () => {
it('价格为 0 时抛出异常', () => {
expect(() => calculateDiscount(0, 10, 'normal')).toThrow('价格必须大于0');
});
it('价格为负数时抛出异常', () => {
expect(() => calculateDiscount(-100, 10, 'normal')).toThrow('价格必须大于0');
});
it('折扣率超过 100 时抛出异常', () => {
expect(() => calculateDiscount(100, 101, 'normal')).toThrow('折扣率必须在0-100之间');
});
it('折扣率为负数时抛出异常', () => {
expect(() => calculateDiscount(100, -1, 'normal')).toThrow('折扣率必须在0-100之间');
});
});
});
模式二:从需求文档生成测试(TDD 正向)
在写代码之前,先让 AI 基于需求文档生成测试,然后再实现代码让测试通过。
需求文档:
## 用户积分系统规则
1. 每消费 1 元获得 1 积分
2. VIP 用户积分翻倍
3. 生日当天积分三倍
4. 单次消费积分上限 1000 分
5. 积分只取整数,不满 1 分丢弃
提示词:
根据以上需求文档,为积分计算函数生成测试用例(TDD方式)。
先不实现函数,只生成测试文件,函数签名为:
calculatePoints(amount: number, isVip: boolean, isBirthday: boolean): number
模式三:AI 发现遗漏的测试场景
这是最有价值的使用方式——让 AI 审查你已有的测试,找出遗漏的场景。
提示词:
审查以下测试文件,找出可能遗漏的测试场景,特别关注:
1. 并发场景
2. 网络异常
3. 数据库事务回滚
4. 大数据量边界
5. 跨时区问题
@test/user.service.spec.ts
模式四:从生产 Bug 生成回归测试
当线上出现 Bug 时,让 AI 帮你把这个 Bug 转化为永久的测试保护:
提示词:
线上发生了以下 Bug:
当用户名包含特殊字符(如 'O'Reilly')时,用户创建接口返回 500 错误。
原因是 SQL 查询中的单引号未转义。
已在 UserService.create() 中修复了此问题。
请为这个修复生成回归测试用例,确保这个 Bug 不会再次出现。
@src/user/user.service.ts
三、AI 辅助 E2E 测试
用 Playwright + AI 生成端到端测试
方法一:Playwright Codegen(录制测试)
# 启动录制模式
npx playwright codegen http://localhost:3000
# 在浏览器中手动操作
# Playwright 自动生成测试代码
方法二:AI 从用户故事生成测试
用户故事:
作为一个注册用户,我应该能够:
1. 在登录页输入邮箱和密码
2. 点击登录按钮
3. 如果凭证正确,跳转到首页,并看到我的用户名
4. 如果凭证错误,看到"用户名或密码错误"的提示信息
请生成 Playwright 测试代码,测试这个用户故事的所有场景。
应用 URL:http://localhost:3000
登录页路径:/login
AI 生成的 Playwright 测试:
import { test, expect } from '@playwright/test';
test.describe('用户登录流程', () => {
test('正确凭证登录成功,跳转到首页', async ({ page }) => {
await page.goto('/login');
await page.fill('[data-testid="email-input"]', 'user@example.com');
await page.fill('[data-testid="password-input"]', 'correctpassword');
await page.click('[data-testid="login-button"]');
await expect(page).toHaveURL('/');
await expect(page.locator('[data-testid="username-display"]')).toBeVisible();
});
test('错误密码显示错误提示', async ({ page }) => {
await page.goto('/login');
await page.fill('[data-testid="email-input"]', 'user@example.com');
await page.fill('[data-testid="password-input"]', 'wrongpassword');
await page.click('[data-testid="login-button"]');
await expect(page).toHaveURL('/login');
await expect(page.locator('[data-testid="error-message"]'))
.toContainText('用户名或密码错误');
});
// ... 更多测试场景
});
四、建立 AI 辅助测试的 CI/CD 流水线
# .github/workflows/test.yml
name: AI-Assisted Test Suite
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Dependencies
run: npm ci
- name: Run Unit Tests
run: npm run test:unit -- --coverage
- name: Check Coverage Threshold
run: |
# 如果覆盖率低于 80%,生成覆盖率报告并触发 AI 分析
coverage=$(cat coverage/coverage-summary.json | jq '.total.lines.pct')
if (( $(echo "$coverage < 80" | bc -l) )); then
echo "覆盖率不足 80%,需要补充测试"
exit 1
fi
- name: Run E2E Tests
run: npx playwright test
- name: Upload Coverage Report
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
五、课后实践任务
- 选取你项目中一个核心业务函数
- 描述给 AI,让它生成完整测试用例(包含边界情况)
- 运行测试,统计覆盖率
- 让 AI 分析遗漏的场景,补充测试
- 目标:将该函数覆盖率提升到 90%+
章节小结:AI 辅助测试的本质是降低写测试的心理和时间成本。最有价值的场景是:从需求文档生成测试(TDD)、发现遗漏场景、从 Bug 生成回归测试。当写测试变得轻松,代码质量的自动守护就真正建立起来了。