Skip to Content

31b - AI测试生成工作流

本文是《AI Agent 实战手册》第 31 章第 2 节。 上一节:31a-AI辅助测试概览 | 下一节:31c-需求到测试用例自动化

概述

AI 测试生成已从”在 Chat 中请求写测试”演进为端到端的自动化工作流——从源代码分析、测试策略选择、框架适配到测试执行与反馈循环,AI Agent 能在数分钟内为整个模块生成覆盖单元、集成和 E2E 三个层级的测试套件。本节深入拆解每种测试类型的 AI 生成工作流,提供 Jest、Vitest、Pytest、Go testing 四大框架的特定模式和提示词模板,帮助你建立可复用的 AI 测试生成管线。


1. AI 测试生成工作流总览

1.1 三层测试生成架构

AI 测试生成并非”一个 prompt 搞定一切”,而是一个分层、迭代的工作流。不同测试类型对 AI 的上下文需求、生成策略和审查重点各不相同。

┌─────────────────────────────────────────────────────────────────────┐ │ AI 测试生成三层架构 │ ├─────────────────────────────────────────────────────────────────────┤ │ │ │ Layer 1: 单元测试生成 │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ 输入:函数签名 + 实现代码 + 类型定义 │ │ │ │ 策略:逐函数分析 → 路径枚举 → 边界推导 → 断言生成 │ │ │ │ 输出:*.test.ts / *_test.py / *_test.go │ │ │ │ 审查重点:断言正确性、边界覆盖、mock 合理性 │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ Layer 2: 集成测试生成 │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ 输入:API 规范 + 服务依赖图 + 数据模型 + 业务流程 │ │ │ │ 策略:端点枚举 → 依赖分析 → 环境策略 → 场景组合 │ │ │ │ 输出:*.integration.test.ts / test_*.py / *_integration_test.go│ │ │ │ 审查重点:环境隔离、数据清理、异步处理、业务语义 │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ Layer 3: E2E 测试生成 │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ 输入:用户流程图 + 页面结构 + 认证方式 + 测试环境配置 │ │ │ │ 策略:流程分解 → 页面对象建模 → 交互脚本 → 断言 + 截图 │ │ │ │ 输出:*.e2e.ts / *.spec.ts (Playwright/Cypress) │ │ │ │ 审查重点:选择器稳定性、等待策略、数据隔离、可维护性 │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘

1.2 工作流核心循环

无论哪种测试类型,AI 测试生成都遵循一个核心循环:

┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ ① 上下文 │────→│ ② 策略 │────→│ ③ 生成 │────→│ ④ 执行 │ │ 收集 │ │ 选择 │ │ 测试 │ │ 验证 │ └──────────┘ └──────────┘ └──────────┘ └────┬─────┘ ┌──────────┐ ┌──────────┐ │ │ ⑥ 人工 │←────│ ⑤ 修复 │←──────────────────────────┘ │ 审查 │ │ 迭代 │ (失败时自动修复,最多 3 轮) └──────────┘ └──────────┘

各步骤详解:

步骤动作AI 角色人工角色
① 上下文收集分析源代码、类型、依赖、已有测试自动分析项目结构提供业务上下文和测试约定
② 策略选择确定测试类型、框架、覆盖策略根据代码特征推荐策略确认或调整策略
③ 生成测试编写测试代码生成完整测试文件
④ 执行验证运行测试、检查通过率自动运行并分析结果
⑤ 修复迭代修复失败的测试分析失败原因并修复判断是测试错误还是代码 bug
⑥ 人工审查审查测试质量验证断言正确性和覆盖完整性

1.3 工具推荐

工具用途价格适用场景
Claude CodeAgent 模式全项目测试生成 + 自动修复循环$20/月(Pro)/ $100/月(Max 5x)深度测试生成、复杂业务逻辑
KiroSpec 驱动测试生成 + Hooks 自动触发免费(50 次/月)/ $19/月(Pro)结构化测试工作流
GitHub Copilot内联补全 + Chat 生成 + 专用测试工作流免费(2000 补全/月)/ $10/月(Pro)日常快速测试生成
CursorTab 补全 + Agent 模式测试生成免费(2000 补全/月)/ $20/月(Pro)IDE 内快速测试迭代
Qodo (原 CodiumAI)上下文感知测试建议 + 行为覆盖分析免费(个人)/ $19/月(Teams)开发者日常单元测试
Vitest下一代 JavaScript/TypeScript 测试框架免费(开源)Vite 项目、ESM 项目
Jest成熟的 JavaScript 测试框架免费(开源)React 项目、企业级项目
PytestPython 测试框架免费(开源)Python 项目
Go testingGo 标准库测试包免费(内置)Go 项目
fast-checkJavaScript/TypeScript PBT 框架免费(开源)前端/Node.js 属性测试
HypothesisPython PBT 框架免费(开源)Python 属性测试
Playwright跨浏览器 E2E 测试框架免费(开源)Web 应用 E2E 测试

2. 单元测试 AI 生成工作流

单元测试是 AI 生成质量最高的测试类型。函数签名和类型信息为 AI 提供了充分的上下文,使其能够系统化地枚举测试路径。

2.1 单元测试生成的五步工作流

步骤 1 步骤 2 步骤 3 步骤 4 步骤 5 ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ 函数分析 │──→│ 路径枚举 │──→│ 测试生成 │──→│ 运行验证 │──→│ 覆盖率 │ │ │ │ │ │ │ │ │ │ 补充 │ │ • 签名 │ │ • 正常路径│ │ • 框架适配│ │ • 全部通过│ │ • 变异测试│ │ • 类型 │ │ • 边界值 │ │ • AAA 模式│ │ • 失败修复│ │ • 盲区补充│ │ • 依赖 │ │ • 异常路径│ │ • 命名规范│ │ • 最多3轮 │ │ • 人工审查│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘

2.2 框架特定模式:Jest

Jest 是 React 生态的标准测试框架,AI 对 Jest 的模式识别最为成熟。

Jest 测试生成提示词模板

你是一个资深 JavaScript/TypeScript 测试工程师。请为以下代码生成 Jest 单元测试。 ## 源代码 ```typescript [粘贴源代码]

项目测试约定

  • 测试框架:Jest + @testing-library/react(如涉及组件)
  • 文件命名:*.test.ts,与源文件同目录
  • 测试风格:describe/it 嵌套,AAA 模式
  • 命名格式:should [预期行为] when [条件]
  • TypeScript 严格模式

生成要求

  1. 覆盖以下场景:
    • 正常路径(至少 2 个典型输入)
    • 边界值(空字符串、空数组、0、负数、最大安全整数)
    • 异常路径(null、undefined、类型错误、超出范围)
    • 异步行为(如涉及 Promise/async)
  2. 断言要具体:
    • ❌ 避免 toBeTruthy()toBeDefined()
    • ✅ 使用 toBe()toEqual()toMatchObject()toThrow()
  3. Mock 策略:
    • 仅 mock 外部依赖(HTTP 请求、数据库、文件系统)
    • 不 mock 被测模块的内部函数
    • 使用 jest.spyOn() 而非 jest.fn() 替换已有实现
  4. 每个 describe 块包含 beforeEach/afterEach 清理逻辑(如需要)
#### Jest 生成示例 **源代码:** ```typescript // src/utils/price-calculator.ts export interface CartItem { name: string; price: number; quantity: number; category: 'food' | 'electronics' | 'clothing'; } export interface DiscountRule { type: 'percentage' | 'fixed'; value: number; minAmount: number; categories?: string[]; } export function calculateTotal( items: CartItem[], discount?: DiscountRule ): { subtotal: number; discountAmount: number; total: number } { const subtotal = items.reduce( (sum, item) => sum + item.price * item.quantity, 0 ); let discountAmount = 0; if (discount && subtotal >= discount.minAmount) { const eligibleAmount = discount.categories ? items .filter(item => discount.categories!.includes(item.category)) .reduce((sum, item) => sum + item.price * item.quantity, 0) : subtotal; discountAmount = discount.type === 'percentage' ? eligibleAmount * (discount.value / 100) : Math.min(discount.value, eligibleAmount); } return { subtotal: Math.round(subtotal * 100) / 100, discountAmount: Math.round(discountAmount * 100) / 100, total: Math.round((subtotal - discountAmount) * 100) / 100, }; }

AI 生成的 Jest 测试:

// src/utils/price-calculator.test.ts import { calculateTotal, CartItem, DiscountRule } from './price-calculator'; describe('calculateTotal', () => { // ── 测试数据工厂 ── const createItem = ( overrides: Partial<CartItem> = {} ): CartItem => ({ name: 'Test Item', price: 10, quantity: 1, category: 'food', ...overrides, }); // ── 正常路径 ── describe('正常计算', () => { it('should calculate subtotal for single item', () => { const items = [createItem({ price: 29.99, quantity: 2 })]; const result = calculateTotal(items); expect(result.subtotal).toBe(59.98); expect(result.discountAmount).toBe(0); expect(result.total).toBe(59.98); }); it('should calculate subtotal for multiple items', () => { const items = [ createItem({ price: 10, quantity: 3 }), createItem({ price: 25.5, quantity: 1, category: 'electronics' }), ]; const result = calculateTotal(items); expect(result.subtotal).toBe(55.5); expect(result.total).toBe(55.5); }); }); // ── 折扣逻辑 ── describe('百分比折扣', () => { it('should apply percentage discount when subtotal meets minimum', () => { const items = [createItem({ price: 100, quantity: 1 })]; const discount: DiscountRule = { type: 'percentage', value: 20, minAmount: 50, }; const result = calculateTotal(items, discount); expect(result.discountAmount).toBe(20); expect(result.total).toBe(80); }); it('should not apply discount when subtotal below minimum', () => { const items = [createItem({ price: 10, quantity: 1 })]; const discount: DiscountRule = { type: 'percentage', value: 20, minAmount: 50, }; const result = calculateTotal(items, discount); expect(result.discountAmount).toBe(0); expect(result.total).toBe(10); }); it('should apply category-specific percentage discount', () => { const items = [ createItem({ price: 50, quantity: 1, category: 'food' }), createItem({ price: 50, quantity: 1, category: 'electronics' }), ]; const discount: DiscountRule = { type: 'percentage', value: 10, minAmount: 50, categories: ['food'], }; const result = calculateTotal(items, discount); expect(result.subtotal).toBe(100); expect(result.discountAmount).toBe(5); // 10% of food items (50) expect(result.total).toBe(95); }); }); describe('固定金额折扣', () => { it('should apply fixed discount', () => { const items = [createItem({ price: 100, quantity: 1 })]; const discount: DiscountRule = { type: 'fixed', value: 15, minAmount: 50, }; const result = calculateTotal(items, discount); expect(result.discountAmount).toBe(15); expect(result.total).toBe(85); }); it('should cap fixed discount at eligible amount', () => { const items = [createItem({ price: 20, quantity: 1, category: 'food' })]; const discount: DiscountRule = { type: 'fixed', value: 50, minAmount: 10, categories: ['food'], }; const result = calculateTotal(items, discount); expect(result.discountAmount).toBe(20); // capped at eligible amount expect(result.total).toBe(0); }); }); // ── 边界值 ── describe('边界条件', () => { it('should return zeros for empty cart', () => { const result = calculateTotal([]); expect(result.subtotal).toBe(0); expect(result.discountAmount).toBe(0); expect(result.total).toBe(0); }); it('should handle zero-price items', () => { const items = [createItem({ price: 0, quantity: 5 })]; const result = calculateTotal(items); expect(result.total).toBe(0); }); it('should handle floating point precision', () => { const items = [createItem({ price: 0.1, quantity: 3 })]; const result = calculateTotal(items); // 0.1 * 3 = 0.30000000000000004 without rounding expect(result.subtotal).toBe(0.3); }); }); // ── 无折扣 ── describe('无折扣场景', () => { it('should work without discount parameter', () => { const items = [createItem({ price: 42, quantity: 1 })]; const result = calculateTotal(items); expect(result.discountAmount).toBe(0); expect(result.total).toBe(42); }); it('should work with undefined discount', () => { const items = [createItem({ price: 42, quantity: 1 })]; const result = calculateTotal(items, undefined); expect(result.total).toBe(42); }); }); });

Jest 关键模式总结

模式说明示例
测试数据工厂createItem() 工厂函数避免重复const createItem = (overrides) => ({...defaults, ...overrides})
describe 嵌套按功能分组,层次清晰describe('百分比折扣', () => { ... })
AAA 模式Arrange-Act-Assert 三段式准备数据 → 调用函数 → 验证结果
精确断言避免模糊断言toBe(20) 而非 toBeTruthy()
边界值覆盖空数组、零值、浮点精度calculateTotal([])

2.3 框架特定模式:Vitest

Vitest 是 2025-2026 年 JavaScript/TypeScript 项目的首选测试框架,尤其适合 Vite 生态和 ESM 项目。其 API 与 Jest 高度兼容,但在性能和 DX 上有显著优势。

Vitest vs Jest:AI 生成时的关键差异

特性JestVitestAI 生成影响
模块系统CommonJS 为主ESM 原生支持Vitest 不需要 jest.mock() 的 hoisting hack
配置jest.config.jsvitest.config.ts(复用 Vite 配置)Vitest 配置更简洁
Mockjest.fn() / jest.spyOn()vi.fn() / vi.spyOn()API 前缀不同
Timer Mockjest.useFakeTimers()vi.useFakeTimers()同上
快照toMatchSnapshot()toMatchSnapshot()(兼容)完全相同
并行执行文件级并行文件级 + 测试级并行Vitest 更快
类型检查需要额外配置内置 typecheck 模式Vitest 可同时验证类型
覆盖率--coverage(需安装 istanbul)@vitest/coverage-v8(V8 原生)Vitest 覆盖率更快

Vitest 测试生成提示词模板

你是一个资深 TypeScript 测试工程师。请为以下代码生成 Vitest 单元测试。 ## 源代码 ```typescript [粘贴源代码]

项目测试约定

  • 测试框架:Vitest(使用 vi 而非 jest)
  • 文件命名:*.test.ts,与源文件同目录
  • 导入方式:import { describe, it, expect, vi, beforeEach } from 'vitest'
  • 测试风格:describe/it 嵌套,AAA 模式
  • 命名格式:should [预期行为] when [条件]

Vitest 特定要求

  1. 使用 vi.fn() 而非 jest.fn()
  2. 使用 vi.spyOn() 而非 jest.spyOn()
  3. 使用 vi.useFakeTimers() 而非 jest.useFakeTimers()
  4. Mock 模块使用 vi.mock() 而非 jest.mock()
  5. 利用 Vitest 的 vi.stubEnv() 处理环境变量
  6. 使用 expect.soft() 进行非中断断言(适合多断言场景)
  7. 异步测试使用 vi.waitFor() 而非手动 setTimeout

覆盖要求

  • 正常路径(至少 2 个典型输入)
  • 边界值(空输入、极值)
  • 异常路径(无效输入、错误处理)
  • 如涉及异步:成功/失败/超时三种场景
#### Vitest 生成示例 **源代码:** ```typescript // src/services/user-service.ts import { db } from '../db'; import { hash } from '../utils/crypto'; export interface CreateUserInput { email: string; password: string; name: string; } export interface User { id: string; email: string; name: string; createdAt: Date; } export async function createUser(input: CreateUserInput): Promise<User> { const existing = await db.user.findUnique({ where: { email: input.email }, }); if (existing) { throw new Error(`User with email ${input.email} already exists`); } if (input.password.length < 8) { throw new Error('Password must be at least 8 characters'); } const hashedPassword = await hash(input.password); return db.user.create({ data: { email: input.email.toLowerCase().trim(), name: input.name.trim(), password: hashedPassword, }, }); }

AI 生成的 Vitest 测试:

// src/services/user-service.test.ts import { describe, it, expect, vi, beforeEach } from 'vitest'; import { createUser, CreateUserInput } from './user-service'; // Mock 外部依赖 vi.mock('../db', () => ({ db: { user: { findUnique: vi.fn(), create: vi.fn(), }, }, })); vi.mock('../utils/crypto', () => ({ hash: vi.fn().mockResolvedValue('hashed_password_123'), })); import { db } from '../db'; import { hash } from '../utils/crypto'; describe('createUser', () => { const validInput: CreateUserInput = { email: 'alice@example.com', password: 'securePass123', name: 'Alice Smith', }; beforeEach(() => { vi.clearAllMocks(); // 默认:用户不存在 vi.mocked(db.user.findUnique).mockResolvedValue(null); // 默认:创建成功 vi.mocked(db.user.create).mockResolvedValue({ id: 'user_001', email: 'alice@example.com', name: 'Alice Smith', createdAt: new Date('2025-01-01'), }); }); // ── 正常路径 ── describe('成功创建用户', () => { it('should create user with valid input', async () => { const result = await createUser(validInput); expect(result).toMatchObject({ id: 'user_001', email: 'alice@example.com', name: 'Alice Smith', }); }); it('should normalize email to lowercase and trim', async () => { await createUser({ ...validInput, email: ' Alice@Example.COM ', }); expect(db.user.create).toHaveBeenCalledWith({ data: expect.objectContaining({ email: 'alice@example.com', }), }); }); it('should trim name whitespace', async () => { await createUser({ ...validInput, name: ' Alice Smith ', }); expect(db.user.create).toHaveBeenCalledWith({ data: expect.objectContaining({ name: 'Alice Smith', }), }); }); it('should hash password before storing', async () => { await createUser(validInput); expect(hash).toHaveBeenCalledWith('securePass123'); expect(db.user.create).toHaveBeenCalledWith({ data: expect.objectContaining({ password: 'hashed_password_123', }), }); }); }); // ── 异常路径 ── describe('错误处理', () => { it('should throw when email already exists', async () => { vi.mocked(db.user.findUnique).mockResolvedValue({ id: 'existing', email: 'alice@example.com', name: 'Existing User', createdAt: new Date(), }); await expect(createUser(validInput)).rejects.toThrow( 'User with email alice@example.com already exists' ); expect(db.user.create).not.toHaveBeenCalled(); }); it('should throw when password is too short', async () => { await expect( createUser({ ...validInput, password: 'short' }) ).rejects.toThrow('Password must be at least 8 characters'); expect(db.user.create).not.toHaveBeenCalled(); }); }); // ── 边界值 ── describe('边界条件', () => { it('should accept password with exactly 8 characters', async () => { await expect( createUser({ ...validInput, password: '12345678' }) ).resolves.toBeDefined(); }); it('should reject password with 7 characters', async () => { await expect( createUser({ ...validInput, password: '1234567' }) ).rejects.toThrow('Password must be at least 8 characters'); }); }); });

Vitest 独有特性在测试生成中的应用

// Vitest 独有:expect.soft() — 非中断断言 it('should return complete user object', async () => { const result = await createUser(validInput); // 所有断言都会执行,即使前面的失败了 expect.soft(result.id).toBeDefined(); expect.soft(result.email).toBe('alice@example.com'); expect.soft(result.name).toBe('Alice Smith'); expect.soft(result.createdAt).toBeInstanceOf(Date); }); // Vitest 独有:vi.stubEnv() — 环境变量 mock it('should use production hash rounds in production', async () => { vi.stubEnv('NODE_ENV', 'production'); await createUser(validInput); expect(hash).toHaveBeenCalledWith('securePass123'); vi.unstubAllEnvs(); }); // Vitest 独有:内联快照 it('should format error message correctly', async () => { vi.mocked(db.user.findUnique).mockResolvedValue({ id: 'existing', email: 'test@test.com', name: 'Test', createdAt: new Date(), }); await expect( createUser({ ...validInput, email: 'test@test.com' }) ).rejects.toThrowErrorMatchingInlineSnapshot( `"User with email test@test.com already exists"` ); });

2.4 框架特定模式:Pytest

Pytest 是 Python 生态中最流行的测试框架,其 fixture 系统和参数化能力使 AI 生成的测试更加简洁和可维护。

Pytest 核心特性与 AI 生成策略

Pytest 特性AI 生成策略优势
FixtureAI 自动识别共享依赖,生成 @pytest.fixture减少重复代码,提高可维护性
参数化AI 用 @pytest.mark.parametrize 合并相似测试一个测试函数覆盖多个输入
异常断言AI 用 pytest.raises() 上下文管理器比 try/except 更简洁
临时目录AI 用 tmp_path fixture 处理文件操作自动清理,测试隔离
MonkeypatchAI 用 monkeypatch 替代手动 mock更 Pythonic 的 mock 方式
ConftestAI 将共享 fixture 提取到 conftest.py跨文件复用

Pytest 测试生成提示词模板

你是一个资深 Python 测试工程师。请为以下代码生成 Pytest 单元测试。 ## 源代码 ```python [粘贴源代码]

项目测试约定

  • 测试框架:Pytest(不使用 unittest 风格)
  • 文件命名:test_*.py,放在 tests/ 目录下
  • 测试函数命名:test_[功能]_[场景]_[预期结果]
  • 使用 Pytest fixture 管理测试数据和依赖
  • 使用 @pytest.mark.parametrize 合并相似测试

Pytest 特定要求

  1. 使用 Pytest 原生断言(assert),不使用 self.assertEqual()
  2. 使用 @pytest.fixture 管理测试数据,而非在每个测试中重复创建
  3. 使用 @pytest.mark.parametrize 参数化边界值测试
  4. 使用 pytest.raises() 测试异常
  5. 使用 monkeypatchunittest.mock.patch 处理外部依赖
  6. 异步测试使用 @pytest.mark.asyncio(需要 pytest-asyncio)
  7. 使用 tmp_path fixture 处理文件操作测试
  8. 使用 capsys fixture 捕获标准输出

覆盖要求

  • 正常路径(至少 2 个典型输入)
  • 边界值(空字符串、空列表、0、None)
  • 异常路径(无效输入、类型错误)
  • 参数化测试覆盖多种输入组合
#### Pytest 生成示例 **源代码:** ```python # src/services/inventory.py from dataclasses import dataclass from enum import Enum from typing import Optional class StockStatus(Enum): IN_STOCK = "in_stock" LOW_STOCK = "low_stock" OUT_OF_STOCK = "out_of_stock" @dataclass class Product: sku: str name: str quantity: int reorder_threshold: int = 10 class InventoryService: def __init__(self, products: Optional[list[Product]] = None): self._products: dict[str, Product] = {} if products: for p in products: self._products[p.sku] = p def add_stock(self, sku: str, amount: int) -> Product: if amount <= 0: raise ValueError("Amount must be positive") if sku not in self._products: raise KeyError(f"Product {sku} not found") product = self._products[sku] product.quantity += amount return product def remove_stock(self, sku: str, amount: int) -> Product: if amount <= 0: raise ValueError("Amount must be positive") if sku not in self._products: raise KeyError(f"Product {sku} not found") product = self._products[sku] if product.quantity < amount: raise ValueError( f"Insufficient stock: have {product.quantity}, need {amount}" ) product.quantity -= amount return product def get_status(self, sku: str) -> StockStatus: if sku not in self._products: raise KeyError(f"Product {sku} not found") product = self._products[sku] if product.quantity == 0: return StockStatus.OUT_OF_STOCK elif product.quantity <= product.reorder_threshold: return StockStatus.LOW_STOCK else: return StockStatus.IN_STOCK def get_reorder_list(self) -> list[Product]: return [ p for p in self._products.values() if p.quantity <= p.reorder_threshold ]

AI 生成的 Pytest 测试:

# tests/test_inventory.py import pytest from src.services.inventory import ( InventoryService, Product, StockStatus, ) # ── Fixtures ── @pytest.fixture def sample_products() -> list[Product]: return [ Product(sku="SKU001", name="Widget A", quantity=50, reorder_threshold=10), Product(sku="SKU002", name="Widget B", quantity=5, reorder_threshold=10), Product(sku="SKU003", name="Widget C", quantity=0, reorder_threshold=5), ] @pytest.fixture def service(sample_products: list[Product]) -> InventoryService: return InventoryService(products=sample_products) @pytest.fixture def empty_service() -> InventoryService: return InventoryService() # ── add_stock 测试 ── class TestAddStock: def test_add_stock_increases_quantity(self, service: InventoryService): result = service.add_stock("SKU001", 10) assert result.quantity == 60 assert result.sku == "SKU001" def test_add_stock_to_zero_quantity_product(self, service: InventoryService): result = service.add_stock("SKU003", 20) assert result.quantity == 20 @pytest.mark.parametrize("invalid_amount", [0, -1, -100]) def test_add_stock_rejects_non_positive_amount( self, service: InventoryService, invalid_amount: int ): with pytest.raises(ValueError, match="Amount must be positive"): service.add_stock("SKU001", invalid_amount) def test_add_stock_unknown_sku_raises_key_error( self, service: InventoryService ): with pytest.raises(KeyError, match="Product UNKNOWN not found"): service.add_stock("UNKNOWN", 10) # ── remove_stock 测试 ── class TestRemoveStock: def test_remove_stock_decreases_quantity(self, service: InventoryService): result = service.remove_stock("SKU001", 10) assert result.quantity == 40 def test_remove_exact_quantity(self, service: InventoryService): result = service.remove_stock("SKU002", 5) assert result.quantity == 0 def test_remove_stock_insufficient_raises_error( self, service: InventoryService ): with pytest.raises(ValueError, match="Insufficient stock"): service.remove_stock("SKU002", 10) def test_remove_from_zero_stock_raises_error( self, service: InventoryService ): with pytest.raises(ValueError, match="Insufficient stock"): service.remove_stock("SKU003", 1) @pytest.mark.parametrize("invalid_amount", [0, -1, -50]) def test_remove_stock_rejects_non_positive_amount( self, service: InventoryService, invalid_amount: int ): with pytest.raises(ValueError, match="Amount must be positive"): service.remove_stock("SKU001", invalid_amount) # ── get_status 测试 ── class TestGetStatus: @pytest.mark.parametrize( "sku, expected_status", [ ("SKU001", StockStatus.IN_STOCK), # quantity=50 > threshold=10 ("SKU002", StockStatus.LOW_STOCK), # quantity=5 <= threshold=10 ("SKU003", StockStatus.OUT_OF_STOCK), # quantity=0 ], ) def test_status_matches_quantity( self, service: InventoryService, sku: str, expected_status: StockStatus, ): assert service.get_status(sku) == expected_status def test_status_at_threshold_is_low_stock(self): """quantity == reorder_threshold 应该是 LOW_STOCK""" product = Product(sku="EDGE", name="Edge", quantity=10, reorder_threshold=10) service = InventoryService(products=[product]) assert service.get_status("EDGE") == StockStatus.LOW_STOCK def test_status_one_above_threshold_is_in_stock(self): """quantity == reorder_threshold + 1 应该是 IN_STOCK""" product = Product(sku="EDGE", name="Edge", quantity=11, reorder_threshold=10) service = InventoryService(products=[product]) assert service.get_status("EDGE") == StockStatus.IN_STOCK def test_status_unknown_sku_raises_key_error( self, service: InventoryService ): with pytest.raises(KeyError): service.get_status("NONEXISTENT") # ── get_reorder_list 测试 ── class TestGetReorderList: def test_returns_low_and_out_of_stock_products( self, service: InventoryService ): reorder = service.get_reorder_list() skus = {p.sku for p in reorder} assert skus == {"SKU002", "SKU003"} def test_empty_service_returns_empty_list( self, empty_service: InventoryService ): assert empty_service.get_reorder_list() == [] def test_all_in_stock_returns_empty_list(self): products = [ Product(sku="A", name="A", quantity=100, reorder_threshold=10), Product(sku="B", name="B", quantity=50, reorder_threshold=5), ] service = InventoryService(products=products) assert service.get_reorder_list() == [] # ── 初始化测试 ── class TestInit: def test_init_with_no_products(self): service = InventoryService() assert service.get_reorder_list() == [] def test_init_with_none(self): service = InventoryService(products=None) assert service.get_reorder_list() == []

Pytest 关键模式总结

模式说明示例
Fixture 层次用 fixture 管理测试数据和服务实例@pytest.fixture def service()
参数化parametrize 合并相似测试@pytest.mark.parametrize("amount", [0, -1, -100])
异常断言pytest.raises + match 验证异常with pytest.raises(ValueError, match="...")
Class 分组TestXxx 类按功能分组class TestAddStock:
边界值参数化将边界值测试参数化("sku, expected_status", [...])
Fixture 组合不同 fixture 组合测试不同场景empty_service vs service

2.5 框架特定模式:Go testing

Go 的测试哲学与 JavaScript/Python 截然不同——没有 describe/it 嵌套,没有断言库(标准库),测试就是普通函数。AI 生成 Go 测试时需要适应这种”极简主义”风格。

Go testing 核心特性与 AI 生成策略

Go testing 特性AI 生成策略与 JS/Python 的差异
Table-Driven TestsAI 将多个测试用例组织为 []struct 表格替代 describe/it 嵌套
SubtestsAI 用 t.Run() 创建子测试类似 it() 但更扁平
无断言库AI 用 if got != want + t.Errorf()expect().toBe()
testify(可选)AI 可使用 assert.Equal() 简化断言第三方库,非标准
TestMainAI 用 TestMain() 管理全局 setup/teardown类似 beforeAll/afterAll
t.Helper()AI 在辅助函数中调用 t.Helper()改善错误报告行号
t.Parallel()AI 标记可并行的测试默认串行,需显式声明
t.Cleanup()AI 用 t.Cleanup() 注册清理函数类似 afterEach

Go testing 测试生成提示词模板

你是一个资深 Go 测试工程师。请为以下代码生成 Go 测试。 ## 源代码 ```go [粘贴源代码]

项目测试约定

  • 测试框架:Go 标准库 testing(不使用 testify,除非明确要求)
  • 文件命名:*_test.go,与源文件同包
  • 测试函数命名:Test[函数名]_[场景](如 TestCalculate_EmptyInput
  • 使用 Table-Driven Tests 组织多个测试用例
  • 使用 t.Run() 创建子测试
  • 辅助函数使用 t.Helper()

Go 特定要求

  1. 使用 Table-Driven Tests 模式:
    tests := []struct { name string input InputType want OutputType wantErr bool }{ ... } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ... }) }
  2. 错误检查模式:
    if err != nil && !tt.wantErr { t.Fatalf("unexpected error: %v", err) } if err == nil && tt.wantErr { t.Fatal("expected error, got nil") }
  3. 使用 t.Helper() 标记辅助函数
  4. 可并行的测试添加 t.Parallel()
  5. 使用 t.Cleanup() 注册清理逻辑
  6. 使用 cmp.Diff() 对比复杂结构体(需要 github.com/google/go-cmp

覆盖要求

  • 正常路径(至少 2 个典型输入)
  • 边界值(空切片、零值、nil)
  • 错误路径(无效输入、错误返回)
  • Table-Driven 覆盖多种输入组合
#### Go testing 生成示例 **源代码:** ```go // pkg/calculator/calculator.go package calculator import ( "errors" "math" ) type Operation string const ( Add Operation = "add" Subtract Operation = "subtract" Multiply Operation = "multiply" Divide Operation = "divide" Power Operation = "power" ) var ErrDivisionByZero = errors.New("division by zero") var ErrUnknownOperation = errors.New("unknown operation") var ErrOverflow = errors.New("result overflow") func Calculate(op Operation, a, b float64) (float64, error) { var result float64 switch op { case Add: result = a + b case Subtract: result = a - b case Multiply: result = a * b case Divide: if b == 0 { return 0, ErrDivisionByZero } result = a / b case Power: result = math.Pow(a, b) default: return 0, ErrUnknownOperation } if math.IsInf(result, 0) || math.IsNaN(result) { return 0, ErrOverflow } return result, nil }

AI 生成的 Go 测试:

// pkg/calculator/calculator_test.go package calculator import ( "math" "testing" ) func TestCalculate(t *testing.T) { t.Parallel() tests := []struct { name string op Operation a, b float64 want float64 wantErr error }{ // ── 加法 ── {name: "add positive numbers", op: Add, a: 2, b: 3, want: 5}, {name: "add negative numbers", op: Add, a: -2, b: -3, want: -5}, {name: "add zero", op: Add, a: 5, b: 0, want: 5}, {name: "add decimals", op: Add, a: 0.1, b: 0.2, want: 0.30000000000000004}, // ── 减法 ── {name: "subtract positive", op: Subtract, a: 10, b: 3, want: 7}, {name: "subtract resulting negative", op: Subtract, a: 3, b: 10, want: -7}, {name: "subtract zero", op: Subtract, a: 5, b: 0, want: 5}, // ── 乘法 ── {name: "multiply positive", op: Multiply, a: 4, b: 5, want: 20}, {name: "multiply by zero", op: Multiply, a: 100, b: 0, want: 0}, {name: "multiply negatives", op: Multiply, a: -3, b: -4, want: 12}, // ── 除法 ── {name: "divide evenly", op: Divide, a: 10, b: 2, want: 5}, {name: "divide with remainder", op: Divide, a: 7, b: 3, want: 7.0 / 3.0}, {name: "divide by zero", op: Divide, a: 10, b: 0, wantErr: ErrDivisionByZero}, // ── 幂运算 ── {name: "power positive", op: Power, a: 2, b: 10, want: 1024}, {name: "power zero exponent", op: Power, a: 5, b: 0, want: 1}, {name: "power negative exponent", op: Power, a: 2, b: -1, want: 0.5}, // ── 错误场景 ── {name: "unknown operation", op: "modulo", a: 10, b: 3, wantErr: ErrUnknownOperation}, // ── 溢出 ── {name: "overflow large power", op: Power, a: 10, b: 309, wantErr: ErrOverflow}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() got, err := Calculate(tt.op, tt.a, tt.b) if tt.wantErr != nil { if err == nil { t.Fatalf("expected error %v, got nil", tt.wantErr) } if err != tt.wantErr { t.Fatalf("expected error %v, got %v", tt.wantErr, err) } return } if err != nil { t.Fatalf("unexpected error: %v", err) } if got != tt.want { t.Errorf("Calculate(%q, %v, %v) = %v, want %v", tt.op, tt.a, tt.b, got, tt.want) } }) } } // ── 边界值专项测试 ── func TestCalculate_BoundaryValues(t *testing.T) { t.Parallel() t.Run("max float64 addition", func(t *testing.T) { t.Parallel() _, err := Calculate(Add, math.MaxFloat64, math.MaxFloat64) if err != ErrOverflow { t.Errorf("expected overflow error, got %v", err) } }) t.Run("very small numbers", func(t *testing.T) { t.Parallel() got, err := Calculate(Add, math.SmallestNonzeroFloat64, math.SmallestNonzeroFloat64) if err != nil { t.Fatalf("unexpected error: %v", err) } if got <= 0 { t.Errorf("expected positive result, got %v", got) } }) t.Run("negative zero division", func(t *testing.T) { t.Parallel() got, err := Calculate(Divide, -0.0, 1) if err != nil { t.Fatalf("unexpected error: %v", err) } if got != 0 { t.Errorf("expected 0, got %v", got) } }) } // ── 辅助函数示例 ── func assertNoError(t *testing.T, err error) { t.Helper() if err != nil { t.Fatalf("unexpected error: %v", err) } } func assertError(t *testing.T, got, want error) { t.Helper() if got == nil { t.Fatalf("expected error %v, got nil", want) } if got != want { t.Fatalf("expected error %v, got %v", want, got) } } func TestCalculate_WithHelpers(t *testing.T) { t.Parallel() t.Run("add with helper", func(t *testing.T) { t.Parallel() got, err := Calculate(Add, 1, 2) assertNoError(t, err) if got != 3 { t.Errorf("got %v, want 3", got) } }) t.Run("divide by zero with helper", func(t *testing.T) { t.Parallel() _, err := Calculate(Divide, 1, 0) assertError(t, err, ErrDivisionByZero) }) }

Go testing 关键模式总结

模式说明示例
Table-Driven Tests用结构体切片组织测试用例tests := []struct{ name string; ... }{ ... }
Subtestst.Run() 创建命名子测试t.Run("add positive", func(t *testing.T) { ... })
Parallelt.Parallel() 标记可并行测试外层和内层都需要调用
Helper 函数t.Helper() 改善错误报告func assertNoError(t *testing.T, err error)
Error Sentinel用预定义错误变量对比if err != ErrDivisionByZero
无断言库if/t.Errorf 替代断言if got != want { t.Errorf(...) }

3. 集成测试 AI 生成工作流

集成测试验证组件间的交互,AI 在这个层级需要更多的上下文引导——不仅需要源代码,还需要 API 规范、数据模型、依赖关系和环境配置。

3.1 集成测试生成的六步工作流

步骤 1 步骤 2 步骤 3 ┌──────────┐ ┌──────────┐ ┌──────────┐ │ 依赖分析 │──→│ 环境策略 │──→│ 场景设计 │ │ │ │ │ │ │ │ • API 规范│ │ • 真实 DB │ │ • 正常流程│ │ • 数据模型│ │ • 内存 DB │ │ • 错误处理│ │ • 服务依赖│ │ • Container│ │ • 并发场景│ │ • 认证方式│ │ • Mock │ │ • 幂等性 │ └──────────┘ └──────────┘ └──────────┘ │ │ ▼ ▼ 步骤 6 步骤 5 步骤 4 ┌──────────┐ ┌──────────┐ ┌──────────┐ │ 数据清理 │←──│ 执行验证 │←──│ 测试生成 │ │ │ │ │ │ │ │ • 事务回滚│ │ • 运行测试│ │ • 框架适配│ │ • 数据删除│ │ • 检查日志│ │ • Setup │ │ • 状态重置│ │ • 修复失败│ │ • Teardown│ └──────────┘ └──────────┘ └──────────┘

3.2 环境策略选择决策树

需要测试数据库交互? ├── 是 ──→ 数据量大或需要真实行为? │ ├── 是 ──→ 使用 Testcontainers(Docker 容器) │ └── 否 ──→ 使用内存数据库(SQLite / H2) └── 否 ──→ 需要测试外部 API? ├── 是 ──→ 使用 HTTP Mock(MSW / WireMock / httptest) └── 否 ──→ 使用函数级 Mock

3.3 框架特定集成测试模式

Vitest/Jest + Supertest(Node.js API 集成测试)

请为以下 Express/Fastify API 端点生成集成测试。 ## API 规范 - 端点:[HTTP 方法] [路径] - 功能:[功能描述] - 请求体:[JSON Schema 或示例] - 响应体:[JSON Schema 或示例] - 认证:[Bearer Token / API Key / 无] - 依赖:[数据库 / 外部 API / 消息队列] ## 环境策略 - 数据库:[Testcontainers PostgreSQL / SQLite 内存 / Mock] - 外部 API:[MSW mock / 真实沙箱环境] - 认证:[测试 token / mock 认证中间件] ## 测试场景要求 1. 正常请求返回正确响应(200/201) 2. 缺少必填字段返回 400 3. 未认证请求返回 401 4. 无权限请求返回 403 5. 资源不存在返回 404 6. 重复创建返回 409(如适用) 7. 并发请求的幂等性(如适用) ## 框架要求 - 使用 [Vitest/Jest] + supertest - 每个测试独立,不依赖执行顺序 - 包含 beforeAll(启动服务器)和 afterAll(关闭服务器) - 每个测试前后清理测试数据

Vitest + Supertest 生成示例:

// tests/integration/users.integration.test.ts import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'; import request from 'supertest'; import { app } from '../../src/app'; import { db } from '../../src/db'; describe('POST /api/users', () => { let server: ReturnType<typeof app.listen>; beforeAll(async () => { await db.migrate.latest(); server = app.listen(0); // 随机端口 }); afterAll(async () => { await server.close(); await db.destroy(); }); beforeEach(async () => { await db('users').truncate(); }); // ── 正常路径 ── it('should create user and return 201', async () => { const response = await request(server) .post('/api/users') .send({ email: 'alice@example.com', name: 'Alice', password: 'securePass123', }) .expect(201); expect(response.body).toMatchObject({ id: expect.any(String), email: 'alice@example.com', name: 'Alice', }); expect(response.body).not.toHaveProperty('password'); // 验证数据库中确实创建了用户 const dbUser = await db('users') .where({ email: 'alice@example.com' }) .first(); expect(dbUser).toBeDefined(); expect(dbUser.name).toBe('Alice'); }); // ── 验证错误 ── it('should return 400 when email is missing', async () => { const response = await request(server) .post('/api/users') .send({ name: 'Alice', password: 'securePass123' }) .expect(400); expect(response.body.error).toContain('email'); }); it('should return 400 when password is too short', async () => { const response = await request(server) .post('/api/users') .send({ email: 'alice@example.com', name: 'Alice', password: 'short', }) .expect(400); expect(response.body.error).toContain('password'); }); // ── 冲突 ── it('should return 409 when email already exists', async () => { // 先创建用户 await request(server) .post('/api/users') .send({ email: 'alice@example.com', name: 'Alice', password: 'securePass123', }) .expect(201); // 再次创建同邮箱用户 const response = await request(server) .post('/api/users') .send({ email: 'alice@example.com', name: 'Alice 2', password: 'anotherPass123', }) .expect(409); expect(response.body.error).toContain('already exists'); }); // ── 幂等性验证 ── it('should handle concurrent creation attempts gracefully', async () => { const userData = { email: 'concurrent@example.com', name: 'Concurrent User', password: 'securePass123', }; const results = await Promise.allSettled([ request(server).post('/api/users').send(userData), request(server).post('/api/users').send(userData), ]); const statuses = results .filter((r): r is PromiseFulfilledResult<any> => r.status === 'fulfilled') .map(r => r.value.status); // 一个成功,一个冲突 expect(statuses.sort()).toEqual([201, 409]); // 数据库中只有一条记录 const count = await db('users') .where({ email: 'concurrent@example.com' }) .count('* as count') .first(); expect(Number(count?.count)).toBe(1); }); });

Pytest + httpx(Python API 集成测试)

# tests/integration/test_users_api.py import pytest import httpx from testcontainers.postgres import PostgresContainer from src.app import create_app from src.db import init_db @pytest.fixture(scope="module") def postgres(): """启动 PostgreSQL 容器""" with PostgresContainer("postgres:16-alpine") as pg: yield pg @pytest.fixture(scope="module") def app(postgres): """创建应用实例""" app = create_app(database_url=postgres.get_connection_url()) init_db(app) return app @pytest.fixture def client(app): """创建测试客户端""" with httpx.Client(app=app, base_url="http://test") as client: yield client @pytest.fixture(autouse=True) def clean_db(app): """每个测试前清理数据""" yield with app.db.begin() as conn: conn.execute("TRUNCATE users CASCADE") class TestCreateUser: def test_create_user_returns_201(self, client: httpx.Client): response = client.post("/api/users", json={ "email": "alice@example.com", "name": "Alice", "password": "securePass123", }) assert response.status_code == 201 data = response.json() assert data["email"] == "alice@example.com" assert "password" not in data def test_missing_email_returns_400(self, client: httpx.Client): response = client.post("/api/users", json={ "name": "Alice", "password": "securePass123", }) assert response.status_code == 400 assert "email" in response.json()["error"] def test_duplicate_email_returns_409(self, client: httpx.Client): user_data = { "email": "alice@example.com", "name": "Alice", "password": "securePass123", } client.post("/api/users", json=user_data) response = client.post("/api/users", json=user_data) assert response.status_code == 409

Go + httptest(Go API 集成测试)

// internal/api/users_test.go package api_test import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "myapp/internal/api" "myapp/internal/db" ) func setupTestServer(t *testing.T) *httptest.Server { t.Helper() testDB := db.NewInMemory() t.Cleanup(func() { testDB.Close() }) handler := api.NewRouter(testDB) return httptest.NewServer(handler) } func TestCreateUser(t *testing.T) { t.Parallel() server := setupTestServer(t) defer server.Close() tests := []struct { name string body map[string]string wantStatus int wantError string }{ { name: "valid user returns 201", body: map[string]string{ "email": "alice@example.com", "name": "Alice", "password": "securePass123", }, wantStatus: http.StatusCreated, }, { name: "missing email returns 400", body: map[string]string{ "name": "Alice", "password": "securePass123", }, wantStatus: http.StatusBadRequest, wantError: "email", }, { name: "short password returns 400", body: map[string]string{ "email": "alice@example.com", "name": "Alice", "password": "short", }, wantStatus: http.StatusBadRequest, wantError: "password", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { body, _ := json.Marshal(tt.body) resp, err := http.Post( server.URL+"/api/users", "application/json", bytes.NewReader(body), ) if err != nil { t.Fatalf("request failed: %v", err) } defer resp.Body.Close() if resp.StatusCode != tt.wantStatus { t.Errorf("status = %d, want %d", resp.StatusCode, tt.wantStatus) } if tt.wantError != "" { var result map[string]string json.NewDecoder(resp.Body).Decode(&result) if errMsg, ok := result["error"]; !ok || errMsg == "" { t.Error("expected error message in response") } } }) } } func TestCreateUser_DuplicateEmail(t *testing.T) { t.Parallel() server := setupTestServer(t) defer server.Close() userData := map[string]string{ "email": "alice@example.com", "name": "Alice", "password": "securePass123", } body, _ := json.Marshal(userData) // 第一次创建 resp1, _ := http.Post(server.URL+"/api/users", "application/json", bytes.NewReader(body)) resp1.Body.Close() if resp1.StatusCode != http.StatusCreated { t.Fatalf("first create: status = %d, want 201", resp1.StatusCode) } // 第二次创建(重复) body2, _ := json.Marshal(userData) resp2, _ := http.Post(server.URL+"/api/users", "application/json", bytes.NewReader(body2)) resp2.Body.Close() if resp2.StatusCode != http.StatusConflict { t.Errorf("duplicate create: status = %d, want 409", resp2.StatusCode) } }

3.4 集成测试环境工具对比

工具语言用途价格适用场景
TestcontainersJava/Go/Node/Python/.NETDocker 容器化测试依赖免费(开源)/ Cloud 付费需要真实数据库/消息队列的集成测试
MSW (Mock Service Worker)JavaScript/TypeScript拦截 HTTP 请求并返回 mock 响应免费(开源)前端集成测试、API mock
WireMockJava(HTTP API 通用)HTTP API mock 服务器免费(开源)/ Cloud 付费后端集成测试、契约测试
httptestGoGo 标准库 HTTP 测试服务器免费(内置)Go API 集成测试
supertestJavaScript/TypeScriptHTTP 断言库免费(开源)Node.js API 集成测试
httpxPython异步 HTTP 客户端(支持 ASGI 测试)免费(开源)Python API 集成测试
SQLite 内存模式通用内存数据库替代免费(内置)轻量级数据库集成测试
Pact多语言消费者驱动的契约测试免费(开源)/ Pactflow 付费微服务间 API 契约验证

4. E2E 测试 AI 生成工作流

E2E 测试验证完整的用户流程,是维护成本最高但业务价值最大的测试类型。AI 在 E2E 测试生成中的核心价值在于:自动化 Page Object 建模、智能选择器生成和自愈能力。

4.1 E2E 测试生成的七步工作流

步骤 1 步骤 2 步骤 3 步骤 4 ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ 流程定义 │──→│ 页面建模 │──→│ 选择器 │──→│ 脚本生成 │ │ │ │ │ │ 策略 │ │ │ │ • 用户故事│ │ • Page │ │ • data- │ │ • 交互 │ │ • 关键路径│ │ Object │ │ testid │ │ • 断言 │ │ • 前置条件│ │ • 组件 │ │ • role │ │ • 等待 │ │ • 测试数据│ │ 抽象 │ │ • text │ │ • 截图 │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ 步骤 7 步骤 6 步骤 5 │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ CI 集成 │←──│ 自愈维护 │←──│ 执行调试 │←───────┘ │ │ │ │ │ │ │ • 并行 │ │ • 选择器 │ │ • 本地运行│ │ • 重试 │ │ 更新 │ │ • 截图对比│ │ • 报告 │ │ • 流程 │ │ • 日志分析│ │ • 通知 │ │ 适配 │ │ • 修复 │ └──────────┘ └──────────┘ └──────────┘

4.2 Playwright E2E 测试生成

Playwright 是 2025-2026 年 E2E 测试的首选框架,其自动等待、多浏览器支持和 Codegen 工具使 AI 生成的测试更加稳定。

Playwright E2E 测试生成提示词模板

你是一个资深 E2E 测试工程师。请为以下用户流程生成 Playwright E2E 测试。 ## 用户流程 流程名称:[名称] 前置条件:[用户状态、数据准备] 步骤: 1. [步骤描述] 2. [步骤描述] 3. [步骤描述] ... 预期结果:[最终验证点] ## 技术要求 - 框架:Playwright(TypeScript) - 使用 Page Object 模式 - 选择器优先级:data-testid > role > text > CSS - 使用 Playwright 自动等待(不使用硬编码 sleep) - 每个关键步骤添加断言 - 失败时自动截图 - 使用 `test.describe` 组织相关测试 - 使用 `test.beforeEach` 处理登录等前置条件 ## 选择器策略 1. 优先使用 `data-testid` 属性:`page.getByTestId('submit-btn')` 2. 其次使用语义角色:`page.getByRole('button', { name: 'Submit' })` 3. 再次使用文本:`page.getByText('Welcome')` 4. 最后使用 CSS(仅在以上都不适用时):`page.locator('.submit-btn')` ## 数据策略 - 测试数据通过 API 创建(不通过 UI) - 每个测试独立,不依赖其他测试的数据 - 测试后清理创建的数据

Playwright 生成示例:电商结账流程

// tests/e2e/checkout.e2e.ts import { test, expect, Page } from '@playwright/test'; // ── Page Objects ── class ProductPage { constructor(private page: Page) {} async goto(productId: string) { await this.page.goto(`/products/${productId}`); } async addToCart() { await this.page.getByRole('button', { name: '加入购物车' }).click(); await expect( this.page.getByText('已添加到购物车') ).toBeVisible(); } async selectQuantity(qty: number) { await this.page.getByTestId('quantity-selector').fill(String(qty)); } } class CartPage { constructor(private page: Page) {} async goto() { await this.page.goto('/cart'); } async getItemCount() { return this.page.getByTestId('cart-item').count(); } async getTotal() { const text = await this.page.getByTestId('cart-total').textContent(); return parseFloat(text?.replace(/[^0-9.]/g, '') ?? '0'); } async proceedToCheckout() { await this.page.getByRole('button', { name: '去结算' }).click(); await expect(this.page).toHaveURL(/\/checkout/); } } class CheckoutPage { constructor(private page: Page) {} async fillShippingAddress(address: { name: string; phone: string; address: string; city: string; }) { await this.page.getByLabel('收件人').fill(address.name); await this.page.getByLabel('手机号').fill(address.phone); await this.page.getByLabel('详细地址').fill(address.address); await this.page.getByLabel('城市').fill(address.city); } async selectPaymentMethod(method: 'credit_card' | 'alipay' | 'wechat') { await this.page.getByTestId(`payment-${method}`).click(); } async placeOrder() { await this.page.getByRole('button', { name: '提交订单' }).click(); } async getOrderConfirmation() { await expect( this.page.getByText('订单提交成功') ).toBeVisible({ timeout: 10000 }); const orderId = await this.page.getByTestId('order-id').textContent(); return orderId; } } // ── 测试套件 ── test.describe('电商结账流程', () => { let productPage: ProductPage; let cartPage: CartPage; let checkoutPage: CheckoutPage; test.beforeEach(async ({ page }) => { productPage = new ProductPage(page); cartPage = new CartPage(page); checkoutPage = new CheckoutPage(page); // 通过 API 登录(比 UI 登录更快更稳定) const response = await page.request.post('/api/auth/login', { data: { email: 'test@example.com', password: 'testPass123' }, }); const { token } = await response.json(); await page.context().addCookies([{ name: 'auth_token', value: token, domain: 'localhost', path: '/', }]); }); test('完整结账流程:添加商品 → 购物车 → 结算 → 订单确认', async ({ page }) => { // Step 1: 浏览商品并添加到购物车 await productPage.goto('prod-001'); await productPage.selectQuantity(2); await productPage.addToCart(); // Step 2: 查看购物车 await cartPage.goto(); const itemCount = await cartPage.getItemCount(); expect(itemCount).toBe(1); const total = await cartPage.getTotal(); expect(total).toBeGreaterThan(0); // Step 3: 进入结算 await cartPage.proceedToCheckout(); // Step 4: 填写收货地址 await checkoutPage.fillShippingAddress({ name: '张三', phone: '13800138000', address: '中关村大街1号', city: '北京', }); // Step 5: 选择支付方式 await checkoutPage.selectPaymentMethod('alipay'); // Step 6: 提交订单 await checkoutPage.placeOrder(); // Step 7: 验证订单确认 const orderId = await checkoutPage.getOrderConfirmation(); expect(orderId).toBeTruthy(); expect(orderId).toMatch(/^ORD-/); // 截图存档 await page.screenshot({ path: 'test-results/checkout-success.png', fullPage: true, }); }); test('购物车为空时不能结算', async ({ page }) => { await cartPage.goto(); const checkoutBtn = page.getByRole('button', { name: '去结算' }); await expect(checkoutBtn).toBeDisabled(); await expect(page.getByText('购物车是空的')).toBeVisible(); }); test('未登录用户结算时跳转到登录页', async ({ page, context }) => { // 清除认证 await context.clearCookies(); await cartPage.goto(); await page.getByRole('button', { name: '去结算' }).click(); await expect(page).toHaveURL(/\/login\?redirect=/); }); });

4.3 E2E 选择器策略对比

选择器类型稳定性可读性维护成本推荐度示例
data-testid⭐⭐⭐⭐⭐⭐⭐⭐⭐首选getByTestId('submit-btn')
ARIA role⭐⭐⭐⭐⭐⭐⭐⭐⭐推荐getByRole('button', { name: 'Submit' })
文本内容⭐⭐⭐⭐⭐⭐⭐⭐可用getByText('Welcome')
Label⭐⭐⭐⭐⭐⭐⭐⭐⭐推荐getByLabel('Email')
CSS 选择器⭐⭐⭐⭐避免locator('.btn-primary')
XPath很高禁止locator('//div[@class="btn"]')

4.4 E2E 测试的 AI 自愈机制

当 UI 变更导致 E2E 测试失败时,AI 可以自动修复:

E2E 测试失败 ┌──────────────────────────────────────────────┐ │ AI 分析失败原因 │ ├──────────────────────────────────────────────┤ │ │ │ 选择器失败?──→ 分析 DOM 变化,更新选择器 │ │ │ │ │ 超时失败?──→ 分析加载时间,调整等待策略 │ │ │ │ │ 断言失败?──→ 分析预期值变化,判断是否为 │ │ │ 有意变更还是 bug │ │ │ │ │ 网络错误?──→ 添加重试逻辑或 mock 不稳定 API │ │ │ └──────────────────────────────────────────────┘

AI 自愈提示词模板

以下 Playwright E2E 测试失败了,请分析原因并修复。 ## 失败信息

[粘贴测试失败的错误信息和堆栈]

## 当前测试代码 ```typescript [粘贴失败的测试代码]

当前页面 HTML(如有)

[粘贴相关的 DOM 片段]

分析要求

  1. 判断失败类型:选择器变更 / 超时 / 断言不匹配 / 网络错误
  2. 如果是选择器变更:
    • 找到新的稳定选择器(优先 data-testid > role > text)
    • 更新测试代码
  3. 如果是超时:
    • 分析是否需要增加等待时间
    • 是否需要添加 waitForResponse / waitForLoadState
  4. 如果是断言不匹配:
    • 判断是有意的 UI 变更还是 bug
    • 如果是有意变更,更新断言
    • 如果是 bug,报告而非修复测试
  5. 提供修复后的完整测试代码
--- ## 5. Test-First Prompting:先写测试再生成代码 Test-First Prompting 是 AI 时代 TDD 的进化形态——先让 AI 生成测试(或人工编写测试),再让 AI 根据测试生成实现代码。这种模式显著提高了 AI 生成代码的正确性和安全性。 ### 5.1 为什么 Test-First Prompting 有效? 传统 AI 代码生成的问题:

传统模式: Prompt → AI 生成代码 → 人工写测试 → 发现 bug → 修复 问题:AI 没有约束,可能生成”看起来对但边界有 bug”的代码

Test-First 模式: Prompt → AI 生成测试 → 人工审查测试 → AI 根据测试生成代码 → 测试自动验证 优势:测试作为 AI 的约束条件,显著减少幻觉代码

| 维度 | 传统模式 | Test-First 模式 | 提升 | |------|---------|----------------|------| | **代码正确性** | 依赖 AI 的"猜测" | 测试约束 AI 的输出 | 显著提高 | | **边界覆盖** | AI 可能遗漏边界 | 测试明确定义边界 | 2-3x | | **安全性** | AI 可能忽略安全检查 | 安全测试强制 AI 实现防护 | 显著提高 | | **可维护性** | 代码和测试可能不一致 | 代码天然与测试对齐 | 提高 | | **审查效率** | 需要审查代码 + 测试 | 先审查测试(更简单),代码自动验证 | 2x | ### 5.2 Test-First Prompting 工作流

┌──────────────────────────────────────────────────────────────────┐ │ Test-First Prompting 工作流 │ ├──────────────────────────────────────────────────────────────────┤ │ │ │ Phase 1: 需求 → 测试 │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ 需求描述 │──→│ AI 生成 │──→│ 人工审查 │ │ │ │ (自然语言) │ │ 测试代码 │ │ 测试正确性│ │ │ └──────────┘ └──────────┘ └──────────┘ │ │ │ │ │ Phase 2: 测试 → 代码 │ │ │ ▼ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ AI 生成 │──→│ 运行测试 │──→│ 全部通过?│ │ │ │ 实现代码 │ │ 自动验证 │ │ 是→完成 │ │ │ └──────────┘ └──────────┘ │ 否→修复 │ │ │ └──────────┘ │ │ │ │ Phase 3: 迭代优化 │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ 覆盖率 │──→│ 补充测试 │──→│ AI 补充 │ │ │ │ 分析 │ │ (边界/PBT)│ │ 实现代码 │ │ │ └──────────┘ └──────────┘ └──────────┘ │ │ │ └──────────────────────────────────────────────────────────────────┘

### 5.3 Test-First 提示词模板 #### Phase 1:从需求生成测试

我需要实现以下功能,请先生成测试代码(不要生成实现代码)。

功能需求

[描述功能需求]

验收标准

  1. [标准 1]
  2. [标准 2]
  3. [标准 3]

测试要求

  • 框架:[Vitest/Jest/Pytest/Go testing]
  • 为每个验收标准生成至少一个测试
  • 包含边界值测试
  • 包含错误处理测试
  • 测试应该是可运行的(即使实现不存在,测试应该能编译但失败)

请只生成测试代码,我会在审查测试后让你生成实现。

#### Phase 2:从测试生成实现

以下是已审查通过的测试代码,请生成能让所有测试通过的实现代码。

测试代码

[粘贴测试代码]

实现要求

  1. 让所有测试通过
  2. 不修改测试代码
  3. 遵循 [项目的编码规范]
  4. 处理测试中未覆盖的边界情况
  5. 添加必要的类型定义和错误处理
### 5.4 Test-First 实战示例 **需求:** 实现一个 URL 短链接服务的核心逻辑 **Phase 1:AI 生成测试** ```typescript // src/services/url-shortener.test.ts import { describe, it, expect, beforeEach } from 'vitest'; import { UrlShortener } from './url-shortener'; describe('UrlShortener', () => { let shortener: UrlShortener; beforeEach(() => { shortener = new UrlShortener(); }); describe('shorten', () => { it('should return a short code for valid URL', () => { const result = shortener.shorten('https://example.com/very/long/path'); expect(result.shortCode).toHaveLength(8); expect(result.shortCode).toMatch(/^[a-zA-Z0-9]+$/); expect(result.originalUrl).toBe('https://example.com/very/long/path'); }); it('should return same short code for same URL', () => { const result1 = shortener.shorten('https://example.com'); const result2 = shortener.shorten('https://example.com'); expect(result1.shortCode).toBe(result2.shortCode); }); it('should return different codes for different URLs', () => { const result1 = shortener.shorten('https://example.com/a'); const result2 = shortener.shorten('https://example.com/b'); expect(result1.shortCode).not.toBe(result2.shortCode); }); it('should throw for invalid URL', () => { expect(() => shortener.shorten('not-a-url')).toThrow('Invalid URL'); }); it('should throw for empty string', () => { expect(() => shortener.shorten('')).toThrow('URL is required'); }); }); describe('resolve', () => { it('should resolve short code to original URL', () => { const { shortCode } = shortener.shorten('https://example.com'); const originalUrl = shortener.resolve(shortCode); expect(originalUrl).toBe('https://example.com'); }); it('should return null for unknown short code', () => { const result = shortener.resolve('unknown1'); expect(result).toBeNull(); }); it('should track access count', () => { const { shortCode } = shortener.shorten('https://example.com'); shortener.resolve(shortCode); shortener.resolve(shortCode); shortener.resolve(shortCode); const stats = shortener.getStats(shortCode); expect(stats?.accessCount).toBe(3); }); }); describe('getStats', () => { it('should return stats for existing short code', () => { const { shortCode } = shortener.shorten('https://example.com'); const stats = shortener.getStats(shortCode); expect(stats).toMatchObject({ originalUrl: 'https://example.com', shortCode, accessCount: 0, createdAt: expect.any(Date), }); }); it('should return null for unknown short code', () => { expect(shortener.getStats('unknown1')).toBeNull(); }); }); });

Phase 2:人工审查测试后,AI 生成实现

// src/services/url-shortener.ts import { createHash } from 'crypto'; export interface ShortenResult { shortCode: string; originalUrl: string; } export interface UrlStats { originalUrl: string; shortCode: string; accessCount: number; createdAt: Date; } export class UrlShortener { private urlToCode = new Map<string, string>(); private codeToEntry = new Map<string, { originalUrl: string; accessCount: number; createdAt: Date; }>(); shorten(url: string): ShortenResult { if (!url) { throw new Error('URL is required'); } try { new URL(url); } catch { throw new Error('Invalid URL'); } // 幂等:相同 URL 返回相同 code const existing = this.urlToCode.get(url); if (existing) { return { shortCode: existing, originalUrl: url }; } const shortCode = this.generateCode(url); this.urlToCode.set(url, shortCode); this.codeToEntry.set(shortCode, { originalUrl: url, accessCount: 0, createdAt: new Date(), }); return { shortCode, originalUrl: url }; } resolve(shortCode: string): string | null { const entry = this.codeToEntry.get(shortCode); if (!entry) return null; entry.accessCount++; return entry.originalUrl; } getStats(shortCode: string): UrlStats | null { const entry = this.codeToEntry.get(shortCode); if (!entry) return null; return { originalUrl: entry.originalUrl, shortCode, accessCount: entry.accessCount, createdAt: entry.createdAt, }; } private generateCode(url: string): string { return createHash('sha256') .update(url) .digest('base64url') .slice(0, 8); } }

5.5 Test-First 与 Spec 驱动的结合

在 Kiro 的 Spec 驱动工作流中,Test-First Prompting 自然融入:

Requirements (验收标准) Design (正确性属性) ←── 这就是"测试规格" Tasks (测试任务 + 实现任务) ├── 先执行测试任务 ←── Test-First │ │ │ ▼ └── 再执行实现任务 ←── 测试约束实现

6. AI 测试生成的高级模式

6.1 批量测试生成

当需要为整个模块或目录生成测试时,使用批量生成模式:

请为以下目录中的所有公开函数生成单元测试: 目录结构:

src/utils/ ├── string-utils.ts (5 个导出函数) ├── date-utils.ts (4 个导出函数) ├── array-utils.ts (6 个导出函数) └── validation.ts (8 个导出函数)

要求: 1. 每个文件生成对应的 *.test.ts 文件 2. 每个函数至少 3 个测试(正常、边界、异常) 3. 使用 Vitest 框架 4. 共享的测试工具函数放在 src/utils/__tests__/helpers.ts 5. 按优先级排序:先生成 validation.ts 的测试(业务关键)

批量生成的执行策略

┌──────────────────────────────────────────────────────────┐ │ 批量测试生成策略 │ ├──────────────────────────────────────────────────────────┤ │ │ │ 策略 1:逐文件生成(推荐) │ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │ │ file1 │→ │ file2 │→ │ file3 │→ │ file4 │ │ │ │ 生成+ │ │ 生成+ │ │ 生成+ │ │ 生成+ │ │ │ │ 验证 │ │ 验证 │ │ 验证 │ │ 验证 │ │ │ └────────┘ └────────┘ └────────┘ └────────┘ │ │ 优点:每步可验证,失败不影响后续 │ │ │ │ 策略 2:全量生成后验证(快速但风险高) │ │ ┌────────────────────────────────┐ ┌────────────┐ │ │ │ 一次性生成所有测试文件 │→ │ 批量运行 │ │ │ └────────────────────────────────┘ │ 修复失败 │ │ │ 优点:速度快 └────────────┘ │ │ 缺点:失败时难以定位问题 │ │ │ │ 推荐:使用策略 1,按业务优先级排序文件 │ └──────────────────────────────────────────────────────────┘

6.2 增量测试生成

当代码变更时,只为变更部分生成新测试:

以下是最近的代码变更(git diff),请为变更部分生成或更新测试。 ## Git Diff ```diff [粘贴 git diff 输出]

现有测试

[粘贴现有测试文件]

要求

  1. 只为新增/修改的函数生成测试
  2. 不修改现有通过的测试
  3. 如果修改了函数签名,更新对应的测试
  4. 如果新增了错误处理路径,添加对应的异常测试
  5. 保持与现有测试的风格一致
### 6.3 从 API 文档生成测试

请根据以下 OpenAPI/Swagger 规范生成 API 集成测试。

OpenAPI 规范

[粘贴 OpenAPI YAML/JSON]

生成要求

  1. 为每个端点生成:
    • 正常请求测试(200/201)
    • 参数验证测试(400)
    • 认证测试(401/403)
    • 资源不存在测试(404)
  2. 使用 [supertest/httpx/httptest] 框架
  3. 请求体和响应体基于 schema 生成
  4. 包含 header 验证(Content-Type、CORS 等)
### 6.4 从数据库 Schema 生成测试

请根据以下数据库 Schema 生成数据访问层的测试。

Schema

[粘贴 CREATE TABLE 语句或 Prisma/Drizzle schema]

生成要求

  1. 为每个表的 CRUD 操作生成测试
  2. 测试约束条件(NOT NULL、UNIQUE、FOREIGN KEY)
  3. 测试级联删除行为
  4. 测试索引是否被正确使用(EXPLAIN 分析)
  5. 使用 [Testcontainers/SQLite 内存] 作为测试数据库
--- ## 7. CI/CD 中的 AI 测试生成集成 ### 7.1 CI/CD 集成架构 将 AI 测试生成集成到 CI/CD 管线中,实现"代码变更 → 自动生成测试 → 自动运行 → 自动报告"的闭环。

┌─────────────────────────────────────────────────────────────────┐ │ CI/CD 中的 AI 测试生成管线 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 开发者 Push 代码 │ │ │ │ │ ▼ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ 代码分析 │──→│ 测试生成 │──→│ 测试执行 │──→│ 报告生成 │ │ │ │ │ │ │ │ │ │ │ │ │ │ • diff │ │ • 单元 │ │ • 并行 │ │ • 覆盖率 │ │ │ │ • 影响 │ │ • 集成 │ │ • 重试 │ │ • 变异 │ │ │ │ 分析 │ │ • E2E │ │ • 超时 │ │ • PR 评论│ │ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │ │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ 质量门(Quality Gate) │ │ │ │ • 覆盖率 ≥ 80% │ │ │ │ • 变异杀死率 ≥ 70% │ │ │ │ • 无新增 flaky 测试 │ │ │ │ • 所有新代码有对应测试 │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘

### 7.2 GitHub Actions 集成示例 ```yaml # .github/workflows/ai-test-generation.yml name: AI Test Generation & Validation on: pull_request: branches: [main] jobs: test-generation: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # 需要完整历史来计算 diff - uses: actions/setup-node@v4 with: node-version: '22' cache: 'npm' - run: npm ci # 运行现有测试 - name: Run existing tests run: npx vitest run --coverage --reporter=json --outputFile=coverage.json # 检查覆盖率门 - name: Check coverage gate run: | COVERAGE=$(node -e " const report = require('./coverage.json'); console.log(report.total.lines.pct); ") echo "Coverage: ${COVERAGE}%" if (( $(echo "$COVERAGE < 80" | bc -l) )); then echo "::warning::Coverage ${COVERAGE}% is below 80% threshold" fi # 运行变异测试(仅对变更文件) - name: Mutation testing on changed files run: | CHANGED_FILES=$(git diff --name-only origin/main...HEAD -- '*.ts' | grep -v '.test.ts' | tr '\n' ',') if [ -n "$CHANGED_FILES" ]; then npx stryker run --mutate "${CHANGED_FILES}" fi # 上传测试报告 - name: Upload test results if: always() uses: actions/upload-artifact@v4 with: name: test-results path: | coverage/ reports/mutation/

7.3 Kiro Hooks 自动触发测试

# .kiro/hooks/auto-test-on-save.yaml # 文件保存时自动运行相关测试 trigger: onFileSave pattern: "src/**/*.ts" action: askAgent prompt: | 文件 {filePath} 已修改。请: 1. 找到对应的测试文件 2. 运行相关测试 3. 如果测试失败,分析原因 4. 如果没有对应测试,建议是否需要生成
# .kiro/hooks/test-on-pr.yaml # PR 创建时自动检查测试覆盖 trigger: onPRCreate action: askAgent prompt: | 新 PR 已创建。请: 1. 分析变更的文件 2. 检查每个变更文件是否有对应测试 3. 运行所有相关测试 4. 生成测试覆盖报告 5. 如果有未覆盖的新代码,建议需要的测试

实战案例:全栈 SaaS 应用的 AI 测试生成

案例背景

一个全栈 SaaS 应用(Next.js + Prisma + PostgreSQL),需要在 2 天内为核心模块建立完整的测试套件。

技术栈

层级技术测试框架
前端Next.js 15 + React 19Vitest + React Testing Library
APINext.js API RoutesVitest + supertest
数据库Prisma + PostgreSQLVitest + Testcontainers
E2EPlaywright
PBTfast-check

执行计划

Day 1 上午:单元测试(4 小时) ├── 工具函数测试(1 小时)—— AI 批量生成 ├── 业务逻辑测试(2 小时)—— AI 生成 + 人工审查 └── React 组件测试(1 小时)—— AI 生成 + 人工补充 Day 1 下午:集成测试(4 小时) ├── API 端点测试(2 小时)—— AI 从 OpenAPI 生成 ├── 数据库操作测试(1.5 小时)—— AI 从 Prisma schema 生成 └── 认证流程测试(0.5 小时)—— 人工设计 + AI 生成 Day 2 上午:E2E 测试(3 小时) ├── 关键用户流程(2 小时)—— AI 生成 Page Objects + 测试 └── 错误场景(1 小时)—— AI 生成 Day 2 下午:PBT + 优化(3 小时) ├── 核心业务属性测试(1.5 小时)—— AI 辅助属性发现 ├── 变异测试评估(1 小时)—— 自动运行 + AI 补充 └── CI/CD 集成(0.5 小时)—— 配置 GitHub Actions

案例分析

关键决策点:

  1. 优先级排序:先测试业务关键路径(支付、认证),再测试工具函数
  2. 环境策略:数据库测试使用 Testcontainers(真实 PostgreSQL),外部 API 使用 MSW mock
  3. AI 角色分配
    • 工具函数:AI 全自动生成(人工快速审查)
    • 业务逻辑:AI 生成骨架 + 人工补充业务断言
    • E2E:AI 生成 Page Objects + 人工定义关键流程
  4. 质量门:覆盖率 ≥ 80%,变异杀死率 ≥ 70%,零 flaky 测试

成果:

指标目标实际
测试文件数47 个
测试用例数312 个
代码覆盖率≥ 80%87%
变异杀死率≥ 70%76%
E2E 关键流程5 个5 个
PBT 属性8 个10 个
总耗时2 天1.5 天

经验总结:

  • AI 生成的单元测试质量最高,几乎不需要修改
  • 集成测试需要人工补充环境配置和数据清理逻辑
  • E2E 测试的 Page Object 由 AI 生成效率很高,但测试流程需要人工定义
  • PBT 属性发现是 AI 最有价值的贡献——AI 能发现人工容易遗漏的属性

避坑指南

❌ 常见错误

  1. 一次性生成所有测试

    • 问题:AI 上下文窗口有限,一次性生成大量测试会导致质量下降、重复代码增多
    • 正确做法:按模块逐步生成,每次生成后运行验证,再生成下一批
  2. 不审查 AI 生成的断言

    • 问题:AI 可能生成”幻觉断言”——看起来合理但验证了错误的行为(如猜测折扣率为 85% 而实际是 80%)
    • 正确做法:每个断言都要对照业务规则验证,尤其是涉及具体数值的断言
  3. 过度依赖 Mock

    • 问题:AI 倾向于 mock 一切外部依赖,导致测试只验证了 mock 的行为而非真实行为
    • 正确做法:优先使用真实依赖(Testcontainers、内存数据库),仅在必要时 mock 外部 API
  4. 忽略测试的可维护性

    • 问题:AI 生成的测试可能包含大量硬编码值、重复代码、缺乏抽象
    • 正确做法:审查后提取测试数据工厂、共享 fixture、Page Object 等抽象
  5. 不运行变异测试评估质量

    • 问题:高覆盖率不等于高质量——测试可能覆盖了代码行但没有验证关键行为
    • 正确做法:定期运行变异测试(Stryker/mutmut),补充能杀死存活变异体的测试
  6. E2E 测试使用不稳定的选择器

    • 问题:AI 可能使用 CSS 类名或 XPath 作为选择器,UI 变更后测试大量失败
    • 正确做法:在 prompt 中明确要求使用 data-testid 或 ARIA role 作为选择器
  7. 测试间存在隐式依赖

    • 问题:AI 生成的测试可能依赖特定的执行顺序或共享状态
    • 正确做法:每个测试独立,使用 beforeEach 重置状态,随机顺序运行验证
  8. 不区分测试类型的生成策略

    • 问题:用同一个 prompt 模板生成所有类型的测试,导致单元测试过于复杂、E2E 测试过于简单
    • 正确做法:为每种测试类型使用专门的 prompt 模板和生成策略

✅ 最佳实践

  1. 建立项目级测试约定文件:在 Steering 规则中定义测试框架、命名规范、目录结构、覆盖率要求,让 AI 每次生成都遵循一致的约定
  2. 采用 Test-First Prompting:先让 AI 生成测试,审查通过后再生成实现代码,显著提高代码正确性
  3. 分层生成,逐步验证:按单元→集成→E2E 的顺序生成,每层生成后运行验证再进入下一层
  4. 用变异测试评估 AI 生成的测试质量:覆盖率只是起点,变异杀死率才是真正的质量指标
  5. 保持人工审查在循环中:AI 生成测试骨架,人工审查业务断言和边界覆盖,形成人机协作的最佳模式
  6. 利用 PBT 补充 example-based 测试:AI 擅长发现属性和设计生成器,PBT 能发现 example-based 测试遗漏的边界
  7. 将测试生成集成到 CI/CD:通过 GitHub Actions 或 Kiro Hooks 自动触发测试生成和运行,形成持续的质量保障
  8. 定期进行测试健康审计:每月审查测试套件的健康度——flaky 率、覆盖盲区、过度 mock、重复测试

相关资源与延伸阅读

  1. Vitest 官方文档 — 下一代 JavaScript 测试框架的完整指南,包含 API 参考、配置和最佳实践

  2. Playwright 官方文档 — 跨浏览器 E2E 测试框架,包含 Codegen、Trace Viewer 和 CI 集成指南

  3. fast-check 官方文档 — JavaScript/TypeScript 属性测试框架,包含 Arbitrary 设计和收缩策略

  4. Hypothesis 官方文档 — Python 属性测试框架,包含策略组合、有状态测试和数据库集成

  5. Stryker Mutator — JavaScript/TypeScript/C# 变异测试框架,评估测试套件的有效性

  6. Testcontainers — 用 Docker 容器管理测试依赖(数据库、消息队列等),支持多语言

  7. MSW (Mock Service Worker) — 拦截网络请求的 API mock 库,支持 REST 和 GraphQL

  8. Testing Library — 以用户行为为中心的测试工具集,支持 React、Vue、Angular 等

  9. Go testing 标准库文档 — Go 官方测试包文档,包含 Table-Driven Tests 和 Benchmark 指南

  10. Pytest 官方文档 — Python 测试框架完整指南,包含 fixture、参数化和插件生态


参考来源


📖 返回 总览与导航 | 上一节:31a-AI辅助测试概览 | 下一节:31c-需求到测试用例自动化

Last updated on