第17讲|AI 辅助测试:让代码质量自动守护自己

1 阅读1分钟

金句:写测试不是在怀疑自己的代码,而是在保护未来的自己不被今天的自己坑。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/

五、课后实践任务

  1. 选取你项目中一个核心业务函数
  2. 描述给 AI,让它生成完整测试用例(包含边界情况)
  3. 运行测试,统计覆盖率
  4. 让 AI 分析遗漏的场景,补充测试
  5. 目标:将该函数覆盖率提升到 90%+

章节小结:AI 辅助测试的本质是降低写测试的心理和时间成本。最有价值的场景是:从需求文档生成测试(TDD)、发现遗漏场景、从 Bug 生成回归测试。当写测试变得轻松,代码质量的自动守护就真正建立起来了。