Skip to Content

31e - 测试 Steering 规则与反模式

本文是《AI Agent 实战手册》第 31 章第 5 节。 上一节:31d-AI辅助PBT | 下一节:32a-AI辅助DevOps概览

概述

AI 生成的测试代码正在成为现代开发工作流的核心组成部分,但”能运行的测试”不等于”有价值的测试”。2025 年的研究表明,AI 生成的测试中约 30-40% 存在质量问题——过度 Mock 导致测试与真实行为脱节、断言不足导致”永远通过”的假绿灯、脆弱的选择器导致无关变更引发大面积失败。Steering 规则是解决这些问题的系统化方案:通过在 CLAUDE.md、Kiro Steering 和 Cursor Rules 中注入测试质量约束,让 AI 在生成测试时自动遵循最佳实践。本节覆盖完整的测试 Steering 规则模板、覆盖率分析工作流、八大测试反模式的识别与修复,以及 AI 生成测试的质量审查清单。


1. 测试 Steering 规则概述

1.1 为什么测试需要专用 Steering 规则

AI 在生成测试代码时面临独特的挑战,与生成业务代码的问题截然不同:

  1. 测试目的理解偏差:AI 倾向于生成”让测试通过”的代码,而非”验证行为正确”的代码
  2. Mock 滥用:AI 默认 Mock 一切外部依赖,导致测试只验证 Mock 的行为而非真实逻辑
  3. 断言质量低:AI 生成的断言往往过于宽泛(如只检查”不为 null”)或过于具体(如检查完整 JSON 字符串)
  4. 复制粘贴倾向:AI 倾向于复制已有测试结构,仅修改少量参数,导致大量冗余测试
  5. 异步处理不当:AI 经常遗漏 await、使用固定延时而非动态等待,导致不稳定测试
  6. 边界值盲区:AI 倾向于测试”正常路径”,忽略空值、极端值、并发等边界场景
  7. 测试隔离不足:AI 生成的测试可能共享状态,导致执行顺序影响结果
  8. 覆盖率虚高:AI 可以轻松生成高覆盖率但低质量的测试,给团队虚假的安全感

1.2 工具推荐

工具用途价格适用场景
Claude CodeAgentic 编码,CLAUDE.md 测试规则按 token 计费(Max $100/月起)复杂项目的测试生成与审查
KiroSpec-Driven 测试,分层 Steering免费(预览期)需求→测试的全链路追溯
CursorAI IDE,.cursorrules 测试规则免费 / Pro $20/月日常测试编写
Vitest现代 JS/TS 测试框架免费(开源)Vite 项目的单元/集成测试
Jest成熟的 JS/TS 测试框架免费(开源)React/Node.js 项目测试
PytestPython 测试框架免费(开源)Python 项目全类型测试
fast-checkJS/TS Property-Based Testing免费(MIT)属性测试和模糊测试
Istanbul / c8JS/TS 代码覆盖率免费(开源)覆盖率收集与报告
Codecov覆盖率分析与 PR 集成免费(开源)/ Team $10/月/用户CI/CD 覆盖率门禁
Stryker变异测试免费(开源)测试质量验证
Testing Library行为驱动的 UI 测试免费(开源)React/Vue/Angular 组件测试
Testcontainers容器化集成测试免费(开源)/ Cloud 按用量数据库/消息队列集成测试

2. 完整测试 Steering 规则模板

2.1 CLAUDE.md 测试规则模板

以下是一个适用于 TypeScript 项目的完整 CLAUDE.md 测试规则模板:

# CLAUDE.md — 测试规则 ## 测试框架与配置 - 单元/集成测试:Vitest 3.x + @testing-library/react - E2E 测试:Playwright 1.x - PBT:fast-check 4.x - 覆盖率:@vitest/coverage-v8 - 测试文件命名:`*.test.ts` / `*.test.tsx`(与源文件同目录) - PBT 文件命名:`*.property.test.ts` ## 测试质量强制规则(最高优先级) ### 禁止过度 Mock - 禁止 Mock 被测模块自身的方法 - 禁止 Mock 纯函数(如工具函数、格式化函数、计算函数) - 仅允许 Mock 以下类型的依赖:网络请求、数据库连接、文件系统、第三方 API、时间/随机数 - 每个测试文件的 Mock 数量不超过 3 个 - 优先使用 Testcontainers 进行真实数据库测试,而非 Mock 数据库层 ### 断言质量要求 - 每个 test case 至少包含 2 个有意义的断言 - 禁止仅断言"不为 null/undefined"——必须断言具体的值或行为 - 禁止仅断言函数"被调用了"——必须同时断言调用参数和返回值 - 使用具体的匹配器:toEqual() 而非 toBeTruthy(),toHaveLength(3) 而非 toBeTruthy() - 错误场景必须断言具体的错误类型和消息,而非仅断言"抛出了异常" ### 测试行为而非实现 - 测试公共 API,不测试私有方法 - 断言输出和副作用,不断言内部状态 - 使用 Testing Library 的 getByRole/getByText,禁止 getByTestId(除非无替代方案) - 重构代码后,测试不应需要修改(除非行为变更) ### 边界值覆盖 - 每个函数的测试必须包含:正常输入、空输入、边界值、错误输入 - 数组/集合:空数组、单元素、大量元素 - 字符串:空字符串、超长字符串、特殊字符、Unicode - 数值:0、负数、最大值、NaN、Infinity - 对象:null、undefined、空对象、嵌套对象 ### 异步测试规则 - 所有异步操作必须使用 await,禁止 .then() 链式调用 - 禁止使用 setTimeout/sleep 等固定延时等待 - 使用 waitFor() / findBy*() 进行动态等待 - 异步错误必须使用 expect(...).rejects.toThrow() - 并发测试必须确保资源隔离 ### 测试隔离 - 每个测试必须独立运行,不依赖其他测试的执行顺序 - 使用 beforeEach 重置状态,禁止在测试间共享可变状态 - 数据库测试使用事务回滚或独立数据库实例 - 文件系统测试使用临时目录 ### 测试命名 - 使用 describe/it 结构,describe 描述被测单元,it 描述行为 - 命名格式:`it('should [预期行为] when [条件]')` - 禁止使用 test1、test2 等无意义命名

2.2 Kiro Steering 测试规则模板

Kiro 的分层 Steering 机制允许按文件类型精确匹配测试规则:

--- inclusion: auto globs: "**/*.test.ts,**/*.test.tsx,**/*.spec.ts,**/*.property.test.ts" --- # 测试文件 Steering 规则 ## 测试结构规范 - 每个测试文件对应一个源文件,放在同一目录下 - 使用 describe 嵌套组织:外层 describe 为模块名,内层 describe 为方法名 - 每个 describe 块内按"正常路径→边界值→错误路径"顺序排列测试 ## Mock 使用约束 - Mock 必须在 describe 块顶部声明,使用 vi.mock() 或 vi.spyOn() - 每个 describe 块最多 Mock 3 个外部依赖 - Mock 的返回值必须与真实 API 的类型签名一致 - 禁止 Mock 被测模块导出的任何函数 ## 断言规范 - 使用 expect().toEqual() 进行深度比较,而非 toBe()(对象/数组) - 使用 expect().toMatchObject() 进行部分匹配(当只关心部分字段时) - 错误断言使用 expect().toThrow(SpecificError) 指定错误类型 - 异步断言使用 await expect().resolves / await expect().rejects ## PBT 规范(*.property.test.ts) - 每个属性测试必须包含 `Validates: Requirements X.Y` 注释 - 生成器优先使用 map/chain,避免 filter(影响收缩效率) - numRuns 默认设置为 500,性能敏感测试可降至 100 - 发现的反例必须固化为回归测试

2.3 Cursor Rules 测试规则模板

--- description: 测试文件的代码生成规则 globs: "**/*.test.ts,**/*.test.tsx,**/*.spec.ts" --- # 测试代码生成规则 ## 核心原则 1. 测试行为,不测试实现 2. 每个测试只验证一个行为 3. 测试名称即文档——读测试名就能理解被测行为 4. Mock 是最后手段,不是默认选择 ## 生成测试时的检查清单 - [ ] 是否覆盖了正常路径? - [ ] 是否覆盖了空输入/边界值? - [ ] 是否覆盖了错误路径? - [ ] 断言是否具体且有意义? - [ ] Mock 是否最小化? - [ ] 异步操作是否正确 await? - [ ] 测试是否可以独立运行? - [ ] 测试名称是否描述了行为? ## 禁止事项 - 禁止生成 snapshot 测试(除非明确要求) - 禁止使用 any 类型的 Mock 返回值 - 禁止在测试中使用 console.log 调试 - 禁止硬编码日期/时间(使用 vi.useFakeTimers())

操作步骤

步骤 1:选择 Steering 平台

根据你使用的 AI 编码工具选择对应的规则文件位置:

工具规则文件位置测试规则放置建议
Claude CodeCLAUDE.md在文件中添加 ## 测试规则 章节
Kiro.kiro/steering/testing.md创建独立的测试 Steering 文件,使用 glob 匹配
Cursor.cursor/rules/testing.mdc创建独立的测试规则文件,使用 glob 匹配
GitHub Copilot.github/copilot-instructions.md在文件中添加测试相关指令

步骤 2:定制规则内容

根据项目技术栈调整规则模板:

请根据以下项目信息,定制测试 Steering 规则: ## 项目技术栈 - 语言:[TypeScript / Python / Go / Rust] - 测试框架:[Vitest / Jest / Pytest / Go testing] - UI 测试库:[Testing Library / Enzyme / Vue Test Utils] - E2E 框架:[Playwright / Cypress / Selenium] - PBT 框架:[fast-check / Hypothesis / rapid / proptest] - 覆盖率工具:[c8 / istanbul / coverage.py / go cover] - CI/CD:[GitHub Actions / GitLab CI / CircleCI] ## 团队约定 - 测试文件位置:[同目录 / __tests__ 目录 / tests 目录] - 最低覆盖率要求:[80% / 90% / 无] - Mock 策略:[最小化 / 允许 / 严格禁止] 请生成适合此项目的完整测试 Steering 规则。

步骤 3:集成到 CI/CD

将 Steering 规则与 CI/CD 管线结合,确保规则被执行:

# .github/workflows/test-quality.yml name: Test Quality Gate on: [pull_request] jobs: test-quality: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install dependencies run: pnpm install - name: Run tests with coverage run: pnpm vitest run --coverage - name: Check coverage thresholds run: | # 确保覆盖率不低于阈值 pnpm vitest run --coverage --coverage.thresholds.lines=80 \ --coverage.thresholds.branches=75 \ --coverage.thresholds.functions=80 - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true

提示词模板

模板 1:生成测试 Steering 规则

你是一位测试工程专家。请为以下项目生成完整的测试 Steering 规则。 ## 项目信息 - 技术栈:[描述] - 测试框架:[框架名] - 团队规模:[人数] - 项目类型:[Web 应用 / API 服务 / CLI 工具 / 库] ## 要求 1. 规则必须覆盖:Mock 策略、断言质量、边界值、异步处理、测试隔离、命名规范 2. 每条规则必须包含"禁止"和"应该"两个方面 3. 规则必须可执行(不是模糊的建议,而是具体的约束) 4. 包含至少 3 个 ❌ 错误示例和 ✅ 正确示例 请按 [CLAUDE.md / Kiro Steering / Cursor Rules] 格式输出。

模板 2:审查 AI 生成的测试

请审查以下 AI 生成的测试代码,检查是否存在以下问题: 1. 过度 Mock:是否 Mock 了不应该 Mock 的依赖? 2. 断言不足:是否每个测试都有具体、有意义的断言? 3. 脆弱性:是否依赖了实现细节或不稳定的选择器? 4. 缺少边界:是否覆盖了空值、极端值、错误路径? 5. 异步问题:是否正确处理了所有异步操作? 6. 测试隔离:测试之间是否存在状态依赖? ## 测试代码 [粘贴测试代码] ## 被测代码 [粘贴被测代码] 对每个发现的问题,请提供: - 问题类型和严重程度(高/中/低) - 具体位置(行号或代码片段) - 修复建议和修正后的代码

3. AI 辅助覆盖率分析工作流

3.1 覆盖率的正确理解

覆盖率是测试质量的必要条件,但不是充分条件。高覆盖率不等于高质量测试——AI 可以轻松生成 100% 行覆盖率但零断言的测试。正确理解覆盖率的层次:

覆盖率类型含义价值AI 容易达到?
行覆盖率(Line)每行代码是否被执行低——只说明代码被运行了⭐ 非常容易
分支覆盖率(Branch)每个 if/else 分支是否被覆盖中——说明条件路径被测试了⭐⭐ 容易
函数覆盖率(Function)每个函数是否被调用低——不说明函数行为正确⭐ 非常容易
条件覆盖率(Condition)复合条件的每个子条件是否独立测试高——说明逻辑组合被验证了⭐⭐⭐ 中等
变异覆盖率(Mutation)代码变异后测试是否能检测到最高——说明测试真正验证了行为⭐⭐⭐⭐ 困难

💡 核心原则:追求”有意义的覆盖率”而非”数字上的覆盖率”。80% 的高质量覆盖率远优于 100% 的低质量覆盖率。

3.2 AI 辅助覆盖率分析的完整工作流

┌─────────────────────────────────────────────────────────────┐ │ AI 辅助覆盖率分析工作流 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 步骤 1: 收集覆盖率数据 │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ 运行测试 │───▶│ 覆盖率工具│───▶│ 覆盖率报告│ │ │ │ (Vitest) │ │ (c8/ist) │ │ (JSON) │ │ │ └──────────┘ └──────────┘ └──────────┘ │ │ │ │ │ 步骤 2: AI 分析覆盖率缺口 │ │ ┌──────────┐ ┌──────────┐ ┌────▼─────┐ │ │ │ 覆盖率报告│───▶│ AI 分析 │───▶│ 缺口列表 │ │ │ │ + 源代码 │ │ 优先级 │ │ (优先级) │ │ │ └──────────┘ └──────────┘ └──────────┘ │ │ │ │ │ 步骤 3: AI 生成补充测试 │ │ ┌──────────┐ ┌──────────┐ ┌────▼─────┐ │ │ │ 缺口列表 │───▶│ AI 生成 │───▶│ 新测试 │ │ │ │ + Steering│ │ 测试代码 │ │ 文件 │ │ │ └──────────┘ └──────────┘ └──────────┘ │ │ │ │ │ 步骤 4: 质量验证 │ │ ┌──────────┐ ┌──────────┐ ┌────▼─────┐ │ │ │ 变异测试 │───▶│ 审查断言 │───▶│ 质量报告 │ │ │ │ (Stryker)│ │ 质量 │ │ │ │ │ └──────────┘ └──────────┘ └──────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘

操作步骤

步骤 1:配置覆盖率收集

Vitest 配置

// vitest.config.ts import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { coverage: { provider: 'v8', // 或 'istanbul' reporter: ['text', 'json', 'html', 'lcov'], reportsDirectory: './coverage', // 覆盖率阈值——CI 中强制执行 thresholds: { lines: 80, branches: 75, functions: 80, statements: 80, }, // 排除不需要覆盖的文件 exclude: [ 'node_modules/', 'dist/', '**/*.d.ts', '**/*.config.*', '**/types/**', '**/__mocks__/**', '**/test-utils/**', ], // 包含需要覆盖的文件(即使没有测试) include: ['src/**/*.ts', 'src/**/*.tsx'], // 全量覆盖:包含没有被任何测试导入的文件 all: true, }, }, });

Pytest 配置

# pyproject.toml [tool.pytest.ini_options] addopts = "--cov=src --cov-report=term-missing --cov-report=html --cov-report=json" [tool.coverage.run] branch = true source = ["src"] omit = ["*/tests/*", "*/migrations/*", "*/__pycache__/*"] [tool.coverage.report] fail_under = 80 show_missing = true exclude_lines = [ "pragma: no cover", "if TYPE_CHECKING:", "if __name__ == .__main__.", ]

步骤 2:让 AI 分析覆盖率缺口

运行测试并生成覆盖率报告后,使用以下 Prompt 让 AI 分析:

请分析以下覆盖率报告,识别最需要补充测试的代码区域。 ## 覆盖率报告摘要 [粘贴 coverage 文本输出] ## 未覆盖的关键文件 [列出覆盖率低于阈值的文件] ## 请按以下优先级排序需要补充测试的区域: 1. 高优先级:业务核心逻辑中未覆盖的分支 2. 中优先级:错误处理路径中未覆盖的分支 3. 低优先级:工具函数中未覆盖的边界值 对每个区域,请提供: - 文件路径和行号范围 - 未覆盖的原因分析(缺少测试 / 死代码 / 难以测试) - 建议的测试策略(单元测试 / 集成测试 / PBT) - 预估的测试编写工作量(小/中/大)

步骤 3:AI 生成补充测试

请为以下未覆盖的代码区域生成补充测试。 ## 未覆盖代码 [粘贴未覆盖的代码片段] ## 已有测试 [粘贴该文件已有的测试] ## 要求 1. 不要重复已有测试覆盖的场景 2. 重点覆盖未测试的分支和边界值 3. 遵循项目的测试 Steering 规则 4. 每个测试必须有具体、有意义的断言 5. 使用 describe/it 结构,命名清晰 请生成补充测试代码。

步骤 4:使用变异测试验证测试质量

# 安装 Stryker(JavaScript/TypeScript) npx stryker init # 运行变异测试 npx stryker run # 查看变异测试报告 # 变异分数(Mutation Score)= 被杀死的变异体 / 总变异体数 # 目标:变异分数 ≥ 70%
# 安装 mutmut(Python) pip install mutmut # 运行变异测试 mutmut run --paths-to-mutate=src/ # 查看结果 mutmut results mutmut html # 生成 HTML 报告

3.3 覆盖率分析的 AI Prompt 模板

你是一位测试覆盖率分析专家。请分析以下项目的测试覆盖率状况, 并提供改进建议。 ## 当前覆盖率数据 - 行覆盖率:[X]% - 分支覆盖率:[X]% - 函数覆盖率:[X]% - 变异分数:[X]%(如果有) ## 覆盖率最低的 5 个文件 [列出文件名和覆盖率] ## 项目类型 [Web 应用 / API 服务 / CLI 工具 / 库] ## 请提供 1. 覆盖率健康度评估(优秀/良好/需改进/危险) 2. 最需要优先补充测试的 3 个区域 3. 每个区域的具体测试策略 4. 预估达到 80% 覆盖率需要的工作量 5. 是否存在"虚假覆盖率"(高覆盖率但低断言质量)的风险

4. AI 生成测试的常见质量问题

在深入八大反模式之前,先了解 AI 生成测试的系统性质量问题:

4.1 AI 生成测试的典型问题分类

问题类别表现根因检测方法
虚假通过测试永远通过,即使代码有 bug断言不足或 Mock 过度变异测试
脆弱失败无关变更导致测试失败依赖实现细节或不稳定选择器重构后运行测试
随机失败同一代码有时通过有时失败异步竞态、共享状态、时间依赖多次运行测试
冗余测试大量测试验证相同行为复制粘贴、缺乏测试设计覆盖率增量分析
慢速测试测试套件运行时间过长不必要的 I/O、缺少并行化测试执行时间分析
不可读测试测试代码难以理解和维护命名不清、结构混乱、魔法数字代码审查

4.2 AI 生成测试的质量评估矩阵

断言质量 低 高 ┌──────────┬──────────┐ 低 │ ❌ 废测试 │ ⚠️ 不完整 │ 覆盖率 │ (删除) │ (补充覆盖) │ ├──────────┼──────────┤ 高 │ ⚠️ 虚假 │ ✅ 高质量 │ │ (加强断言) │ (保持) │ └──────────┴──────────┘

5. 八大测试反模式详解

反模式 1:过度 Mock(Over-Mocking)

问题描述

过度 Mock 是 AI 生成测试中最常见的反模式。AI 倾向于 Mock 一切外部依赖——包括不应该被 Mock 的纯函数、工具类和被测模块自身的方法。结果是测试只验证了 Mock 的行为,而非真实代码的逻辑。

危害等级:🔴 高

  • 测试与真实行为完全脱节
  • 代码有 bug 但测试仍然通过
  • 重构时 Mock 需要同步更新,维护成本高
  • 给团队虚假的安全感

❌ 错误示例

// ❌ 过度 Mock:Mock 了被测模块自身的依赖和纯函数 import { calculateTotal } from '../utils/math'; import { formatCurrency } from '../utils/format'; import { OrderService } from '../services/order'; // Mock 了纯函数——这些函数没有副作用,不应该被 Mock vi.mock('../utils/math', () => ({ calculateTotal: vi.fn().mockReturnValue(100), })); vi.mock('../utils/format', () => ({ formatCurrency: vi.fn().mockReturnValue('$100.00'), })); describe('OrderService', () => { it('should create order', async () => { const service = new OrderService(); const order = await service.createOrder({ items: [{ price: 50, quantity: 2 }], }); // ❌ 这个测试只验证了 Mock 返回了预设值 // 如果 calculateTotal 的实现有 bug,这个测试仍然通过 expect(order.total).toBe(100); expect(order.displayTotal).toBe('$100.00'); }); });

✅ 正确示例

// ✅ 最小化 Mock:只 Mock 真正的外部依赖(数据库) import { OrderService } from '../services/order'; import { createMockDb } from '../test-utils/db'; describe('OrderService', () => { it('should calculate order total correctly', async () => { // 只 Mock 数据库连接——这是真正的外部依赖 const mockDb = createMockDb(); mockDb.orders.create.mockResolvedValue({ id: '1' }); const service = new OrderService(mockDb); const order = await service.createOrder({ items: [ { price: 50, quantity: 2 }, { price: 30, quantity: 1 }, ], }); // ✅ 真实的 calculateTotal 和 formatCurrency 被调用 // 如果计算逻辑有 bug,这个测试会失败 expect(order.total).toBe(130); expect(order.displayTotal).toBe('$130.00'); expect(mockDb.orders.create).toHaveBeenCalledWith( expect.objectContaining({ total: 130 }) ); }); });

检测方法

请审查以下测试文件中的 Mock 使用情况: [粘贴测试代码] 检查标准: 1. 是否 Mock 了纯函数(无副作用的工具函数)? 2. 是否 Mock 了被测模块自身导出的方法? 3. Mock 数量是否超过 3 个? 4. 是否可以用真实实现替代某些 Mock? 对每个不必要的 Mock,请说明为什么不需要 Mock,并提供移除后的代码。

Steering 规则

## Mock 约束 - 仅 Mock 以下类型:网络请求、数据库、文件系统、第三方 API、时间/随机数 - 禁止 Mock 纯函数(无副作用的计算/格式化/验证函数) - 禁止 Mock 被测模块自身的方法 - 每个 describe 块最多 3 个 vi.mock() - 优先使用依赖注入而非全局 Mock

反模式 2:断言不足(Insufficient Assertions)

问题描述

AI 生成的测试经常只包含最基本的断言——检查返回值”不为 null”、函数”被调用了”、或者”没有抛出异常”。这些测试在代码有 bug 时仍然通过,因为它们没有验证具体的行为和输出。

危害等级:🔴 高

  • 测试永远通过,无法检测回归
  • 变异测试分数极低(变异体存活率高)
  • 覆盖率数字好看但实际无保护作用
  • 代码审查时容易被忽略(“有测试了”)

❌ 错误示例

// ❌ 断言不足:测试通过但不验证任何有意义的行为 describe('UserService', () => { it('should get user', async () => { const user = await userService.getById('123'); // ❌ 只检查不为 null——即使返回了错误的用户也会通过 expect(user).not.toBeNull(); }); it('should update user', async () => { const result = await userService.update('123', { name: 'Alice' }); // ❌ 只检查"真值"——任何非空对象都会通过 expect(result).toBeTruthy(); }); it('should delete user', async () => { // ❌ 只检查"不抛异常"——即使删除没有生效也会通过 await expect(userService.delete('123')).resolves.not.toThrow(); }); it('should validate email', () => { const result = validateEmail('test@example.com'); // ❌ 只检查类型——true 和 false 都是 boolean expect(typeof result).toBe('boolean'); }); });

✅ 正确示例

// ✅ 充分断言:验证具体的值、结构和行为 describe('UserService', () => { it('should return user with correct fields when found', async () => { const user = await userService.getById('123'); // ✅ 断言具体的字段值 expect(user).toEqual({ id: '123', name: 'Alice', email: 'alice@example.com', role: 'user', createdAt: expect.any(Date), }); }); it('should update user name and return updated user', async () => { const result = await userService.update('123', { name: 'Bob' }); // ✅ 断言更新后的具体值 expect(result.name).toBe('Bob'); // ✅ 断言未修改的字段保持不变 expect(result.email).toBe('alice@example.com'); // ✅ 断言更新时间被刷新 expect(result.updatedAt).toBeInstanceOf(Date); expect(result.updatedAt.getTime()).toBeGreaterThan( result.createdAt.getTime() ); }); it('should remove user and return confirmation', async () => { await userService.delete('123'); // ✅ 断言删除后确实查不到了 const deleted = await userService.getById('123'); expect(deleted).toBeNull(); }); it('should return true for valid email formats', () => { // ✅ 断言具体的返回值 expect(validateEmail('test@example.com')).toBe(true); expect(validateEmail('user+tag@domain.co.uk')).toBe(true); }); it('should return false for invalid email formats', () => { expect(validateEmail('')).toBe(false); expect(validateEmail('not-an-email')).toBe(false); expect(validateEmail('@no-local.com')).toBe(false); expect(validateEmail('no-domain@')).toBe(false); }); });

检测方法

使用变异测试是检测断言不足的最有效方法:

# Stryker(JavaScript/TypeScript) npx stryker run --mutate 'src/services/user.ts' # 如果变异分数 < 60%,说明断言严重不足 # 存活的变异体 = 测试未能检测到的代码变更

Steering 规则

## 断言质量 - 每个 test case 至少 2 个具体断言 - 禁止仅使用 toBeTruthy() / not.toBeNull() / toBeDefined() - 必须断言具体值:toEqual()、toBe()、toHaveLength()、toContain() - 错误测试必须断言错误类型和消息:toThrow(SpecificError) - 函数调用断言必须包含参数检查:toHaveBeenCalledWith(具体参数)

反模式 3:脆弱测试(Brittle Tests)

问题描述

脆弱测试是指在被测代码行为没有变化的情况下,因为无关的变更(如 CSS 类名修改、HTML 结构调整、错误消息措辞变化)而失败的测试。AI 生成的测试特别容易出现这个问题,因为 AI 倾向于使用最具体的选择器和最精确的字符串匹配。

危害等级:🟡 中高

  • 无关变更导致大量测试失败,浪费调查时间
  • 团队对测试失去信任,开始忽略失败
  • 维护成本高——每次 UI 调整都需要更新测试
  • 最终导致测试被注释掉或删除

❌ 错误示例

// ❌ 脆弱测试:依赖 CSS 类名、DOM 结构和精确字符串 describe('LoginForm', () => { it('should show error message', async () => { render(<LoginForm />); // ❌ 依赖 CSS 类名——样式重构就会失败 const button = document.querySelector('.btn-primary.login-btn'); fireEvent.click(button!); // ❌ 依赖精确的 DOM 结构——HTML 调整就会失败 const error = document.querySelector( 'div.form-container > div.error-wrapper > span.error-text' ); expect(error).toBeTruthy(); // ❌ 依赖精确的错误消息文本——措辞修改就会失败 expect(error?.textContent).toBe( 'Invalid credentials. Please check your email and password and try again.' ); }); it('should render form correctly', () => { const { container } = render(<LoginForm />); // ❌ 快照测试——任何 UI 变更都会失败 expect(container).toMatchSnapshot(); }); });

✅ 正确示例

// ✅ 稳健测试:基于角色和行为,不依赖实现细节 describe('LoginForm', () => { it('should show error when credentials are invalid', async () => { render(<LoginForm />); // ✅ 使用角色选择器——不依赖 CSS 类名 const emailInput = screen.getByRole('textbox', { name: /email/i }); const passwordInput = screen.getByLabelText(/password/i); const submitButton = screen.getByRole('button', { name: /log in/i }); await userEvent.type(emailInput, 'wrong@example.com'); await userEvent.type(passwordInput, 'wrongpassword'); await userEvent.click(submitButton); // ✅ 使用 role="alert" 查找错误消息——不依赖 DOM 结构 const errorMessage = await screen.findByRole('alert'); // ✅ 使用 toContain 而非精确匹配——允许措辞微调 expect(errorMessage.textContent).toContain('Invalid credentials'); }); it('should disable submit button while loading', async () => { render(<LoginForm />); const submitButton = screen.getByRole('button', { name: /log in/i }); await userEvent.click(submitButton); // ✅ 断言行为(按钮禁用),不断言样式 expect(submitButton).toBeDisabled(); }); });

检测方法

请审查以下测试代码的脆弱性: [粘贴测试代码] 检查以下脆弱性指标: 1. 是否使用了 CSS 选择器(.class-name, #id)查找元素? 2. 是否使用了精确的 DOM 路径(div > span > a)? 3. 是否使用了精确的字符串匹配(toBe('完整的长字符串'))? 4. 是否使用了 snapshot 测试? 5. 是否依赖了特定的 HTML 属性(data-testid 除外)? 对每个脆弱点,请提供更稳健的替代方案。

Steering 规则

## 测试稳健性 - UI 测试优先使用 getByRole > getByLabelText > getByText > getByTestId - 禁止使用 CSS 选择器(.class, #id)查找测试元素 - 禁止使用 DOM 路径选择器(div > span > a) - 字符串断言使用 toContain() 或正则匹配,避免精确匹配长字符串 - 禁止使用 snapshot 测试(除非测试序列化格式的稳定性) - 错误消息断言使用错误码而非错误文本

反模式 4:测试实现而非行为(Testing Implementation Details)

问题描述

测试实现细节意味着测试关注的是代码”如何做”而非”做了什么”。AI 生成的测试经常检查内部状态、私有方法调用顺序、具体的数据结构实现等,导致代码重构时测试大面积失败——即使外部行为完全没有变化。

危害等级:🟡 中高

  • 重构成本极高——改内部实现就要改测试
  • 测试变成了代码的”镜像”,失去了独立验证的价值
  • 阻碍代码改进——开发者害怕改动导致测试失败
  • 测试代码与业务代码紧耦合

❌ 错误示例

// ❌ 测试实现细节:检查内部状态和调用顺序 describe('ShoppingCart', () => { it('should add item to cart', () => { const cart = new ShoppingCart(); cart.addItem({ id: '1', name: 'Book', price: 29.99 }); // ❌ 检查内部数据结构——如果改用 Map 存储就会失败 expect(cart['_items']).toEqual([ { id: '1', name: 'Book', price: 29.99 }, ]); // ❌ 检查内部计数器——实现细节 expect(cart['_itemCount']).toBe(1); }); it('should calculate total', () => { const cart = new ShoppingCart(); // ❌ 监听私有方法的调用——重构就会失败 const spy = vi.spyOn(cart as any, '_applyDiscount'); cart.addItem({ id: '1', name: 'Book', price: 100 }); cart.applyPromoCode('SAVE10'); const total = cart.getTotal(); // ❌ 断言私有方法被调用了——这是实现细节 expect(spy).toHaveBeenCalledWith(10); // ❌ 断言私有方法的调用次数 expect(spy).toHaveBeenCalledTimes(1); }); });

✅ 正确示例

// ✅ 测试行为:只关注公共 API 的输入和输出 describe('ShoppingCart', () => { it('should include added item in cart contents', () => { const cart = new ShoppingCart(); cart.addItem({ id: '1', name: 'Book', price: 29.99 }); // ✅ 通过公共 API 验证行为 expect(cart.getItemCount()).toBe(1); expect(cart.getItems()).toContainEqual( expect.objectContaining({ id: '1', name: 'Book' }) ); }); it('should apply 10% discount with SAVE10 promo code', () => { const cart = new ShoppingCart(); cart.addItem({ id: '1', name: 'Book', price: 100 }); cart.applyPromoCode('SAVE10'); // ✅ 只断言最终结果——不关心折扣是如何计算的 expect(cart.getTotal()).toBe(90); }); it('should not apply invalid promo code', () => { const cart = new ShoppingCart(); cart.addItem({ id: '1', name: 'Book', price: 100 }); cart.applyPromoCode('INVALID'); // ✅ 验证行为:无效码不影响价格 expect(cart.getTotal()).toBe(100); }); });

检测方法

请审查以下测试是否存在"测试实现细节"的问题: [粘贴测试代码] 检查标准: 1. 是否访问了私有属性(通过 ['_prop'] 或 (obj as any).prop)? 2. 是否监听了私有方法(vi.spyOn(obj as any, '_method'))? 3. 是否断言了方法调用顺序而非最终结果? 4. 如果重构内部实现(如数组改 Map),测试是否会失败? 5. 测试是否在"重复"被测代码的逻辑? 对每个问题,请提供基于行为的替代测试。

Steering 规则

## 行为驱动测试 - 只测试公共 API(public 方法和导出函数) - 禁止访问私有属性:禁止 obj['_private'] 和 (obj as any).private - 禁止监听私有方法:禁止 vi.spyOn(obj as any, '_method') - 断言最终结果,不断言中间步骤 - 重构内部实现后,测试不应需要修改(除非公共行为变更)

反模式 5:缺少边界测试(Missing Boundary Tests)

问题描述

AI 生成的测试倾向于使用”正常”的输入值——有效的邮箱、合理的数字、非空的字符串。但真正的 bug 往往隐藏在边界值中:空数组、零值、超长字符串、Unicode 字符、并发请求等。AI 缺乏”攻击者思维”,不会主动探索这些边缘场景。

危害等级:🟡 中

  • 正常路径测试通过,但边界场景崩溃
  • 生产环境遇到意外输入时出现未处理的异常
  • 安全漏洞(如缓冲区溢出、注入攻击)未被发现
  • PBT 可以弥补,但 AI 默认不生成 PBT

❌ 错误示例

// ❌ 只测试正常路径,缺少边界值 describe('paginate', () => { it('should return paginated results', () => { const items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; const result = paginate(items, { page: 1, pageSize: 3 }); expect(result.data).toEqual([1, 2, 3]); expect(result.totalPages).toBe(4); }); it('should return second page', () => { const items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; const result = paginate(items, { page: 2, pageSize: 3 }); expect(result.data).toEqual([4, 5, 6]); }); // ❌ 缺少:空数组、page=0、page=-1、pageSize=0、 // pageSize 大于总数、超出范围的页码等边界测试 });

✅ 正确示例

// ✅ 完整的边界值覆盖 describe('paginate', () => { // 正常路径 it('should return correct page of results', () => { const items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; const result = paginate(items, { page: 1, pageSize: 3 }); expect(result.data).toEqual([1, 2, 3]); expect(result.totalPages).toBe(4); expect(result.hasNext).toBe(true); expect(result.hasPrev).toBe(false); }); // 边界值:空数组 it('should handle empty array', () => { const result = paginate([], { page: 1, pageSize: 10 }); expect(result.data).toEqual([]); expect(result.totalPages).toBe(0); expect(result.hasNext).toBe(false); }); // 边界值:单元素 it('should handle single item', () => { const result = paginate([42], { page: 1, pageSize: 10 }); expect(result.data).toEqual([42]); expect(result.totalPages).toBe(1); }); // 边界值:页码超出范围 it('should return empty data for page beyond total', () => { const items = [1, 2, 3]; const result = paginate(items, { page: 999, pageSize: 10 }); expect(result.data).toEqual([]); }); // 边界值:pageSize 等于总数 it('should return all items when pageSize equals total', () => { const items = [1, 2, 3]; const result = paginate(items, { page: 1, pageSize: 3 }); expect(result.data).toEqual([1, 2, 3]); expect(result.totalPages).toBe(1); }); // 错误路径:无效参数 it('should throw for page < 1', () => { expect(() => paginate([1], { page: 0, pageSize: 10 })).toThrow(); expect(() => paginate([1], { page: -1, pageSize: 10 })).toThrow(); }); it('should throw for pageSize < 1', () => { expect(() => paginate([1], { page: 1, pageSize: 0 })).toThrow(); expect(() => paginate([1], { page: 1, pageSize: -5 })).toThrow(); }); // PBT 补充:属性测试覆盖更多边界 it('should never return more items than pageSize', () => { fc.assert( fc.property( fc.array(fc.integer()), fc.integer({ min: 1, max: 100 }), fc.integer({ min: 1, max: 100 }), (items, page, pageSize) => { const result = paginate(items, { page, pageSize }); return result.data.length <= pageSize; } ) ); }); });

Steering 规则

## 边界值覆盖 - 每个函数测试必须包含:正常输入 + 空输入 + 边界值 + 错误输入 - 数组/集合边界:[]、[单元素]、[大量元素] - 字符串边界:''、'a'、超长字符串(10000+字符)、Unicode('🎉')、特殊字符 - 数值边界:0、-1、Number.MAX_SAFE_INTEGER、NaN、Infinity - 对象边界:null、undefined、{}、深层嵌套 - 日期边界:闰年、时区边界、Unix 纪元、远未来日期 - 对于核心业务逻辑,补充 PBT 属性测试覆盖更多边界

反模式 6:复制粘贴测试(Copy-Paste Tests)

问题描述

AI 生成测试时倾向于复制已有测试的结构,仅修改少量参数值。结果是大量几乎相同的测试用例,增加了维护成本但没有增加覆盖率。更糟糕的是,如果原始测试有缺陷(如断言不足),所有复制的测试都会继承同样的缺陷。

危害等级:🟡 中

  • 测试文件膨胀,可读性下降
  • 维护成本高——修改一个模式需要改所有副本
  • 原始缺陷被复制到所有测试中
  • 给人”测试很多”的错觉,实际覆盖面窄

❌ 错误示例

// ❌ 复制粘贴:10 个几乎相同的测试 describe('validateAge', () => { it('should return true for age 18', () => { expect(validateAge(18)).toBe(true); }); it('should return true for age 19', () => { expect(validateAge(19)).toBe(true); }); it('should return true for age 20', () => { expect(validateAge(20)).toBe(true); }); it('should return true for age 25', () => { expect(validateAge(25)).toBe(true); }); it('should return true for age 30', () => { expect(validateAge(30)).toBe(true); }); it('should return false for age 17', () => { expect(validateAge(17)).toBe(false); }); it('should return false for age 16', () => { expect(validateAge(16)).toBe(false); }); it('should return false for age 15', () => { expect(validateAge(15)).toBe(false); }); // ... 还有更多几乎相同的测试 });

✅ 正确示例

// ✅ 使用参数化测试 + 聚焦边界值 describe('validateAge', () => { // 参数化:有效年龄(聚焦边界值而非随机值) it.each([ [18, true, '最小有效年龄'], [19, true, '刚超过最小年龄'], [120, true, '最大合理年龄'], [17, false, '刚低于最小年龄'], [0, false, '零值'], [-1, false, '负数'], [121, false,'超过最大合理年龄'], ])('validateAge(%i) should return %s (%s)', (age, expected, _desc) => { expect(validateAge(age)).toBe(expected); }); // 错误输入单独测试 it('should throw for non-integer input', () => { expect(() => validateAge(18.5)).toThrow('Age must be an integer'); expect(() => validateAge(NaN)).toThrow('Age must be a number'); }); // PBT 补充:属性测试覆盖所有有效范围 it('should accept all ages in [18, 120]', () => { fc.assert( fc.property( fc.integer({ min: 18, max: 120 }), (age) => validateAge(age) === true ) ); }); it('should reject all ages below 18', () => { fc.assert( fc.property( fc.integer({ min: -1000, max: 17 }), (age) => validateAge(age) === false ) ); }); });

检测方法

请审查以下测试文件是否存在复制粘贴问题: [粘贴测试代码] 检查标准: 1. 是否有多个测试结构几乎相同,只是输入值不同? 2. 是否可以用 it.each / @pytest.mark.parametrize / 表驱动测试替代? 3. 重复的测试是否都在测试同一个边界条件? 4. 是否可以用 PBT 替代大量的示例测试? 请提供重构后的精简版本。

Steering 规则

## 避免复制粘贴 - 3 个以上结构相同的测试必须使用参数化:it.each() / test.each() - 参数化测试的用例应聚焦边界值,而非随机的正常值 - 每个参数化用例必须有描述性标签(第三个参数) - 对于输入空间大的函数,优先使用 PBT 而非大量示例测试 - 测试辅助函数(factory/builder)应提取到 test-utils 目录

反模式 7:异步问题(Async Testing Issues)

问题描述

异步测试是 AI 生成测试中最容易出错的领域。常见问题包括:遗漏 await 导致断言在异步操作完成前执行、使用固定延时(setTimeout)等待异步操作、未正确处理 Promise 拒绝、以及竞态条件导致测试结果不确定。

危害等级:🔴 高

  • 测试”通过”但实际上断言从未执行(遗漏 await)
  • 固定延时导致 CI 环境中测试不稳定
  • 竞态条件导致测试随机失败
  • 未捕获的 Promise 拒绝导致测试框架报告误导性错误

❌ 错误示例

// ❌ 异步问题集锦 describe('UserAPI', () => { // ❌ 问题 1:遗漏 await——断言在异步操作完成前执行 it('should fetch user', () => { // 没有 await!这个测试永远通过,因为断言在 Promise resolve 前执行 const user = fetchUser('123'); expect(user).toEqual({ id: '123', name: 'Alice' }); // 比较的是 Promise 对象 }); // ❌ 问题 2:使用固定延时等待 it('should show loading then data', async () => { render(<UserProfile userId="123" />); expect(screen.getByText('Loading...')).toBeInTheDocument(); // ❌ 固定延时——在慢速 CI 环境中可能不够,在快速环境中浪费时间 await new Promise(resolve => setTimeout(resolve, 2000)); expect(screen.getByText('Alice')).toBeInTheDocument(); }); // ❌ 问题 3:未正确处理 Promise 拒绝 it('should handle error', () => { // 没有 await 和 rejects!如果 fetchUser 抛出异常,测试仍然通过 expect(fetchUser('invalid')).toThrow(); }); // ❌ 问题 4:回调风格的异步测试(容易遗漏 done 调用) it('should process queue', (done) => { processQueue((result) => { expect(result).toBe('success'); // 如果 processQueue 从不调用回调,测试会超时而非明确失败 done(); }); }); });

✅ 正确示例

// ✅ 正确的异步测试 describe('UserAPI', () => { // ✅ 正确使用 await it('should fetch user data', async () => { const user = await fetchUser('123'); expect(user).toEqual({ id: '123', name: 'Alice' }); }); // ✅ 使用 waitFor / findBy 动态等待 it('should show loading then data', async () => { render(<UserProfile userId="123" />); // 初始状态 expect(screen.getByText('Loading...')).toBeInTheDocument(); // ✅ 动态等待——自动轮询直到元素出现或超时 const userName = await screen.findByText('Alice'); expect(userName).toBeInTheDocument(); // ✅ 确认 loading 消失 await waitFor(() => { expect(screen.queryByText('Loading...')).not.toBeInTheDocument(); }); }); // ✅ 正确处理 Promise 拒绝 it('should throw for invalid user id', async () => { await expect(fetchUser('invalid')).rejects.toThrow( 'User not found' ); }); // ✅ 使用 async/await 替代回调 it('should process queue successfully', async () => { const result = await processQueueAsync(); expect(result).toBe('success'); }); // ✅ 并发测试:确保资源隔离 it('should handle concurrent requests', async () => { const [user1, user2] = await Promise.all([ fetchUser('1'), fetchUser('2'), ]); expect(user1.id).toBe('1'); expect(user2.id).toBe('2'); }); });

检测方法

# ESLint 规则检测遗漏的 await # .eslintrc.js { "rules": { "@typescript-eslint/no-floating-promises": "error", "@typescript-eslint/await-thenable": "error", "no-return-await": "warn", "require-await": "warn" } }

Steering 规则

## 异步测试 - 所有异步测试函数必须标记为 async - 所有 Promise 必须使用 await,禁止 .then() 链 - 禁止使用 setTimeout / sleep 等固定延时等待 - UI 测试使用 findBy* / waitFor() 动态等待 - Promise 拒绝使用 await expect().rejects.toThrow() - 禁止使用 done 回调风格——使用 async/await - 并发测试必须确保资源隔离(独立数据、独立端口)

反模式 8:不稳定测试(Flaky Tests)

问题描述

不稳定测试(Flaky Tests)是指在代码没有任何变更的情况下,有时通过有时失败的测试。这是所有测试反模式中最具破坏性的一种——它直接摧毁团队对测试套件的信任。2025 年的研究表明,约 90% 的测试不稳定性源于时序问题、环境依赖、共享状态和外部服务不可用(Autonoma Research ,2025)。AI 生成的测试尤其容易产生不稳定性,因为 AI 不会考虑 CI 环境与本地环境的差异。

危害等级:🔴 极高

  • 团队开始忽略测试失败(“可能又是 flaky test”)
  • 真正的 bug 被淹没在噪音中
  • CI/CD 管线被迫添加自动重试,掩盖问题
  • 开发者浪费大量时间调查虚假失败
  • 最终导致测试套件被废弃

❌ 错误示例

// ❌ 不稳定测试集锦 describe('NotificationService', () => { // ❌ 问题 1:依赖系统时间 it('should send notification at correct time', () => { const notification = createNotification({ message: 'Hello', sendAt: new Date(), // 依赖当前时间——不同时刻运行结果不同 }); // 如果测试在午夜前后运行,日期可能跨天 expect(notification.scheduledDate).toBe( new Date().toISOString().split('T')[0] ); }); // ❌ 问题 2:依赖外部服务 it('should fetch latest exchange rate', async () => { // 直接调用真实 API——网络问题或 API 变更会导致失败 const rate = await fetch('https://api.exchangerate.host/latest'); const data = await rate.json(); expect(data.rates.USD).toBeGreaterThan(0); }); // ❌ 问题 3:测试间共享状态 let sharedCounter = 0; it('should increment counter', () => { sharedCounter++; expect(sharedCounter).toBe(1); }); it('should have counter at 1', () => { // 依赖上一个测试的执行——并行运行或顺序变化会失败 expect(sharedCounter).toBe(1); }); // ❌ 问题 4:依赖随机数 it('should generate unique ID', () => { const id1 = generateId(); const id2 = generateId(); // 极小概率碰撞,但在大量 CI 运行中终会发生 expect(id1).not.toBe(id2); }); // ❌ 问题 5:端口冲突 it('should start server', async () => { // 硬编码端口——并行测试或端口被占用时失败 const server = await startServer({ port: 3000 }); expect(server.isRunning()).toBe(true); await server.close(); }); });

✅ 正确示例

// ✅ 稳定测试 describe('NotificationService', () => { // ✅ 使用 fake timers 控制时间 it('should send notification at correct time', () => { vi.useFakeTimers(); vi.setSystemTime(new Date('2025-06-15T10:00:00Z')); const notification = createNotification({ message: 'Hello', sendAt: new Date(), }); expect(notification.scheduledDate).toBe('2025-06-15'); vi.useRealTimers(); }); // ✅ Mock 外部服务 it('should fetch latest exchange rate', async () => { vi.spyOn(global, 'fetch').mockResolvedValue( new Response(JSON.stringify({ rates: { USD: 1.08, EUR: 1.0 }, })) ); const rate = await getExchangeRate('USD'); expect(rate).toBe(1.08); }); // ✅ 每个测试独立设置状态 let counter: Counter; beforeEach(() => { counter = new Counter(); // 每个测试独立实例 }); it('should increment counter from zero', () => { counter.increment(); expect(counter.value).toBe(1); }); it('should start at zero', () => { expect(counter.value).toBe(0); // 不依赖其他测试 }); // ✅ 使用确定性种子控制随机数 it('should generate unique IDs', () => { const ids = new Set( Array.from({ length: 1000 }, () => generateId()) ); // 断言唯一性比例而非绝对唯一 expect(ids.size).toBeGreaterThan(990); }); // ✅ 使用动态端口 it('should start server on available port', async () => { const server = await startServer({ port: 0 }); // 0 = 系统分配 expect(server.isRunning()).toBe(true); expect(server.port).toBeGreaterThan(0); await server.close(); }); });

检测方法

# 方法 1:多次运行测试检测不稳定性 for i in {1..10}; do npx vitest run --reporter=json 2>/dev/null | \ jq '.testResults[].assertionResults[] | select(.status == "failed") | .fullName' done # 方法 2:使用 --repeat 标志(Vitest 2.x+) npx vitest run --repeat=5 # 方法 3:使用 Jest 的 --forceExit 检测未清理的资源 npx jest --forceExit --detectOpenHandles # 方法 4:Pytest 的 flaky 检测 pip install pytest-repeat pytest-randomly pytest --count=10 -x # 运行 10 次,首次失败即停止

Steering 规则

## 测试稳定性 - 禁止依赖系统时间:使用 vi.useFakeTimers() / freezegun - 禁止调用真实外部 API:Mock 所有网络请求 - 禁止测试间共享可变状态:每个测试用 beforeEach 重置 - 禁止硬编码端口号:使用 port: 0 让系统分配 - 禁止依赖文件系统中的固定路径:使用 tmp 目录 - 禁止依赖测试执行顺序:每个测试必须可独立运行 - 使用 vi.useFakeTimers() 控制所有时间相关逻辑 - 数据库测试使用事务回滚或 Testcontainers 隔离

6. 测试代码审查清单

6.1 AI 生成测试的审查流程

每次 AI 生成测试代码后,使用以下清单进行审查:

┌─────────────────────────────────────────────────────────────┐ │ AI 生成测试审查清单 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 第 1 层:结构检查(自动化) │ │ □ 测试文件命名是否符合规范?(*.test.ts) │ │ □ 是否使用 describe/it 结构? │ │ □ 测试名称是否描述了行为? │ │ □ 是否有 TypeScript 类型错误? │ │ │ │ 第 2 层:Mock 检查 │ │ □ Mock 数量是否合理?(≤ 3 个/describe) │ │ □ 是否 Mock 了不应该 Mock 的纯函数? │ │ □ Mock 返回值类型是否与真实 API 一致? │ │ □ 是否可以用真实实现替代某些 Mock? │ │ │ │ 第 3 层:断言检查 │ │ □ 每个测试是否有 ≥ 2 个具体断言? │ │ □ 是否存在 toBeTruthy/not.toBeNull 等弱断言? │ │ □ 错误测试是否断言了具体的错误类型和消息? │ │ □ 是否断言了副作用(如数据库写入、事件发送)? │ │ │ │ 第 4 层:覆盖检查 │ │ □ 是否覆盖了正常路径? │ │ □ 是否覆盖了空输入/边界值? │ │ □ 是否覆盖了错误路径? │ │ □ 是否有遗漏的重要分支? │ │ │ │ 第 5 层:稳定性检查 │ │ □ 是否依赖系统时间? │ │ □ 是否依赖外部服务? │ │ □ 是否存在共享可变状态? │ │ □ 异步操作是否正确 await? │ │ □ 是否使用了固定延时等待? │ │ │ │ 第 6 层:可维护性检查 │ │ □ 是否存在复制粘贴测试?(应使用 it.each) │ │ □ 是否测试了实现细节而非行为? │ │ □ 测试辅助代码是否提取到了 test-utils? │ │ □ 测试是否可以在重构后继续通过? │ │ │ └─────────────────────────────────────────────────────────────┘

6.2 审查 Prompt 模板

你是一位资深测试工程师。请按照以下清单审查 AI 生成的测试代码, 并给出评分和改进建议。 ## 审查清单 1. Mock 合理性(1-5 分) 2. 断言质量(1-5 分) 3. 边界值覆盖(1-5 分) 4. 异步正确性(1-5 分) 5. 测试稳定性(1-5 分) 6. 可维护性(1-5 分) ## 测试代码 [粘贴测试代码] ## 被测代码 [粘贴被测代码] ## 输出格式 ### 总评分:[X]/30 ### 各维度评分 | 维度 | 分数 | 问题 | |------|------|------| | Mock 合理性 | X/5 | [问题描述] | | ... | ... | ... | ### 具体问题列表 1. [严重程度] [问题描述] [行号] [修复建议] ### 修复后的测试代码 [完整的修复版本]

7. Steering 规则模板库

7.1 通用测试 Steering 规则(适用于所有项目)

# 测试质量 Steering 规则(通用版) ## 测试哲学 - 测试是代码的文档——读测试就能理解被测代码的行为 - 测试是安全网——重构时测试应该保护你,而非阻碍你 - 测试是设计工具——难以测试的代码通常设计有问题 ## 强制规则 1. 每个 test case 只验证一个行为 2. 每个 test case 至少 2 个具体断言 3. Mock 仅用于外部依赖(网络/数据库/文件系统/第三方 API) 4. 禁止测试私有方法和内部状态 5. 禁止使用固定延时等待异步操作 6. 禁止测试间共享可变状态 7. 边界值测试必须覆盖:空值、零值、极端值、错误输入 8. 测试命名格式:should [行为] when [条件] ## 禁止清单 - ❌ vi.mock() 纯函数 - ❌ expect(x).toBeTruthy() 作为唯一断言 - ❌ snapshot 测试(除非明确要求) - ❌ setTimeout/sleep 等待异步 - ❌ 访问私有属性 obj['_prop'] - ❌ 硬编码端口号/文件路径 - ❌ console.log 调试代码残留 - ❌ any 类型的 Mock 返回值

7.2 React/前端测试 Steering 规则

# React 测试 Steering 规则 ## 组件测试 - 使用 @testing-library/react,禁止 Enzyme - 选择器优先级:getByRole > getByLabelText > getByText > getByTestId - 禁止使用 CSS 选择器查找元素 - 用户交互使用 @testing-library/user-event,禁止 fireEvent - 测试用户可见的行为,不测试组件内部状态 ## Hook 测试 - 使用 renderHook() 测试自定义 hooks - 测试 hook 的返回值和副作用,不测试内部实现 - 异步 hook 使用 waitFor() 等待状态更新 ## 状态管理测试 - Zustand/Redux store 测试:验证 action 后的 state 变化 - React Query 测试:Mock API 响应,验证缓存和重试行为 - 禁止直接测试 reducer 的内部逻辑(测试通过 action 触发的行为)

7.3 API/后端测试 Steering 规则

# API 测试 Steering 规则 ## 集成测试 - 使用 Supertest(Express/Fastify)或 httpx(FastAPI) - 数据库测试使用 Testcontainers,禁止 Mock 数据库层 - 每个测试使用事务回滚或独立数据库实例 - 测试完整的请求-响应周期,包括中间件 ## API 端点测试 - 每个端点测试:成功响应 + 验证失败 + 认证失败 + 权限不足 - 断言响应状态码、响应体结构、响应头 - 错误响应必须断言错误码和消息格式 ## 安全测试 - SQL 注入:测试特殊字符输入('; DROP TABLE--) - XSS:测试 HTML/JS 注入输入 - 认证:测试过期 token、无效 token、缺少 token - 授权:测试越权访问(用户 A 访问用户 B 的资源)

7.4 Python 测试 Steering 规则

# Python 测试 Steering 规则 ## 框架与工具 - 使用 pytest,禁止 unittest(除非维护遗留代码) - 参数化使用 @pytest.mark.parametrize - PBT 使用 Hypothesis - Mock 使用 pytest-mock(unittest.mock 的 pytest 封装) - 异步测试使用 pytest-asyncio ## fixture 规范 - 使用 pytest fixture 管理测试数据和依赖 - fixture scope 选择:function(默认)> class > module > session - 数据库 fixture 使用 transaction rollback - 禁止在 conftest.py 中定义过多全局 fixture ## 类型检查 - 测试代码也必须通过 mypy 类型检查 - Mock 对象使用 spec=True 确保类型安全 - 禁止 # type: ignore 在测试代码中

7.5 Go 测试 Steering 规则

# Go 测试 Steering 规则 ## 测试结构 - 使用表驱动测试(Table-Driven Tests) - 测试函数命名:Test[FunctionName]_[Scenario] - 子测试使用 t.Run("scenario", func(t *testing.T) {...}) - 并行测试使用 t.Parallel() ## Mock 策略 - 优先使用接口(interface)实现 Mock,而非 Mock 框架 - 仅 Mock 外部依赖(HTTP 客户端、数据库连接) - 使用 httptest.NewServer() 测试 HTTP 客户端 - 使用 testcontainers-go 进行数据库集成测试 ## PBT - 使用 rapid 库进行属性测试 - 生成器使用 rapid.Custom() 构建领域特定生成器 - 属性测试函数命名:TestProperty_[PropertyName] ## 错误处理测试 - 测试所有返回 error 的函数的错误路径 - 使用 errors.Is() 和 errors.As() 断言错误类型 - 禁止仅断言 err != nil——必须断言具体错误

实战案例:电商订单系统的测试质量改进

背景

一个电商团队使用 AI(Claude Code + Kiro)生成了订单系统的测试套件。初始状态:

  • 行覆盖率:92%
  • 测试数量:156 个
  • CI 通过率:约 85%(15% 的运行会有随机失败)
  • 变异测试分数:38%(极低——说明大量测试是”假绿灯”)

案例分析

第 1 步:诊断问题

团队使用测试审查清单对 AI 生成的测试进行系统审查,发现以下问题分布:

反模式受影响测试数占比严重程度
过度 Mock4227%🔴 高
断言不足5837%🔴 高
脆弱测试128%🟡 中
测试实现细节2315%🟡 中
缺少边界测试3120%🟡 中
复制粘贴1812%🟢 低
异步问题85%🔴 高
不稳定测试117%🔴 极高

注:一个测试可能同时存在多个问题。

第 2 步:添加 Steering 规则

团队在 .kiro/steering/testing.md 中添加了测试 Steering 规则,核心约束:

--- inclusion: always globs: "**/*.test.ts,**/*.test.tsx" --- # 订单系统测试规则 ## Mock 约束 - 仅 Mock:数据库(Prisma)、支付网关(Stripe)、邮件服务(SendGrid) - 禁止 Mock:价格计算、库存检查、订单状态机等业务逻辑 - 数据库测试优先使用 Testcontainers + PostgreSQL ## 断言要求 - 订单创建测试必须断言:订单 ID、状态、总价、商品列表、创建时间 - 支付测试必须断言:支付状态、金额、货币、交易 ID - 错误测试必须断言:错误码(如 ORDER_NOT_FOUND)和 HTTP 状态码 ## 边界值 - 价格测试必须包含:0 元订单、最大金额(999999.99)、负数价格 - 库存测试必须包含:0 库存、1 库存、超量购买 - 优惠券测试必须包含:过期券、已用券、叠加券、100% 折扣

第 3 步:AI 重新生成测试

使用 Steering 规则后,让 AI 重新生成测试。以订单创建为例:

改进前(AI 原始生成):

it('should create order', async () => { vi.mock('../services/pricing'); // Mock 了价格计算 vi.mock('../services/inventory'); // Mock 了库存检查 vi.mock('../db/prisma'); // Mock 了数据库 const order = await createOrder(mockOrderInput); expect(order).toBeTruthy(); // 弱断言 });

改进后(Steering 规则约束下的 AI 生成):

describe('createOrder', () => { // 使用 Testcontainers 的真实数据库 let db: PrismaClient; beforeAll(async () => { db = await setupTestDatabase(); // Testcontainers }); afterAll(async () => { await db.$disconnect(); }); beforeEach(async () => { await db.order.deleteMany(); // 清理数据 await seedProducts(db); // 种子数据 }); it('should create order with correct total and status', async () => { // 仅 Mock 支付网关 vi.spyOn(stripeClient, 'createPaymentIntent').mockResolvedValue({ id: 'pi_test', status: 'succeeded', }); const order = await createOrder(db, { items: [ { productId: 'prod_1', quantity: 2 }, // 单价 50 { productId: 'prod_2', quantity: 1 }, // 单价 30 ], couponCode: 'SAVE10', // 10% 折扣 }); // 具体断言 expect(order.id).toMatch(/^ord_/); expect(order.status).toBe('confirmed'); expect(order.subtotal).toBe(130); // 50*2 + 30*1 expect(order.discount).toBe(13); // 130 * 10% expect(order.total).toBe(117); // 130 - 13 expect(order.items).toHaveLength(2); expect(order.createdAt).toBeInstanceOf(Date); // 验证数据库持久化 const saved = await db.order.findUnique({ where: { id: order.id } }); expect(saved).not.toBeNull(); expect(saved!.total).toBe(117); }); // 边界值测试 it('should reject order with zero quantity', async () => { await expect( createOrder(db, { items: [{ productId: 'prod_1', quantity: 0 }] }) ).rejects.toThrow('Quantity must be at least 1'); }); it('should reject order exceeding inventory', async () => { await expect( createOrder(db, { items: [{ productId: 'prod_1', quantity: 9999 }] }) ).rejects.toThrow('Insufficient inventory'); }); it('should handle expired coupon gracefully', async () => { const order = await createOrder(db, { items: [{ productId: 'prod_1', quantity: 1 }], couponCode: 'EXPIRED_CODE', }); // 过期券不应用折扣,但订单仍然创建成功 expect(order.discount).toBe(0); expect(order.total).toBe(50); expect(order.couponWarning).toBe('Coupon expired'); }); });

第 4 步:结果对比

指标改进前改进后变化
行覆盖率92%88%↓ 4%(移除了虚假覆盖)
分支覆盖率71%83%↑ 12%
变异测试分数38%76%↑ 38%(核心改进)
测试数量15698↓ 37%(移除冗余测试)
CI 通过率85%99.2%↑ 14.2%
平均运行时间45s32s↓ 29%

关键发现

  • 行覆盖率下降了 4%,但变异测试分数翻倍——说明之前的高覆盖率是虚假的
  • 测试数量减少了 37%,但检测能力大幅提升
  • CI 通过率从 85% 提升到 99.2%,几乎消除了不稳定测试
  • 运行时间减少 29%,因为移除了不必要的 Mock 设置和冗余测试

避坑指南

❌ 常见错误

  1. 追求 100% 覆盖率

    • 问题:为了达到 100% 覆盖率而生成大量低质量测试,包括测试 getter/setter、配置文件、类型定义等
    • 正确做法:设定合理的覆盖率目标(80% 行覆盖率 + 75% 分支覆盖率),重点关注业务核心逻辑的覆盖质量
  2. 用 Mock 让测试通过

    • 问题:当测试失败时,通过添加更多 Mock 来”修复”测试,而非修复被测代码
    • 正确做法:测试失败时先分析是代码 bug 还是测试问题,Mock 只用于隔离外部依赖
  3. 忽略变异测试

    • 问题:只看行覆盖率,不验证测试是否真正能检测到代码变更
    • 正确做法:定期运行变异测试(Stryker/mutmut),变异分数 ≥ 70% 才说明测试有效
  4. Snapshot 测试滥用

    • 问题:对 UI 组件使用 snapshot 测试,任何 UI 变更都导致 snapshot 更新,团队习惯性地 --updateSnapshot
    • 正确做法:用行为测试替代 snapshot 测试,只在测试序列化格式稳定性时使用 snapshot
  5. 测试代码不做代码审查

    • 问题:业务代码严格审查,但测试代码直接合并,导致低质量测试积累
    • 正确做法:测试代码与业务代码同等审查标准,使用测试审查清单
  6. AI 生成测试后不验证

    • 问题:AI 生成的测试直接提交,不检查断言质量和覆盖范围
    • 正确做法:每次 AI 生成测试后,运行变异测试验证质量,使用审查清单检查
  7. 不稳定测试用重试”修复”

    • 问题:在 CI 中添加自动重试(retry: 3)来掩盖不稳定测试
    • 正确做法:找到不稳定的根因(时序、共享状态、外部依赖)并修复
  8. 测试和代码由同一个 AI 对话生成

    • 问题:在同一个 AI 对话中生成代码和测试,AI 可能让测试”配合”代码而非独立验证
    • 正确做法:代码和测试在不同的 AI 对话中生成,或先写测试再生成代码(TDD 模式)

✅ 最佳实践

  1. Steering 规则先行:在项目开始时就配置测试 Steering 规则,而非事后补救
  2. 变异测试作为质量门:在 CI 中集成变异测试,变异分数低于阈值则阻止合并
  3. TDD 模式与 AI 结合:先让 AI 从需求生成测试,再让 AI 生成通过测试的代码
  4. PBT 补充边界覆盖:对核心业务逻辑使用 Property-Based Testing 自动探索边界值
  5. 测试金字塔平衡:单元测试 70% + 集成测试 20% + E2E 测试 10%
  6. 定期测试健康检查:每月运行一次完整的测试质量审查(覆盖率 + 变异测试 + 不稳定性检测)
  7. 测试代码也要重构:当测试变得难以维护时,像重构业务代码一样重构测试
  8. Spec 驱动的测试追溯:每个测试关联到需求文档的验收标准,确保需求变更时测试同步更新

相关资源与延伸阅读

  1. Testing Library 官方文档  — 行为驱动测试的权威指南,包含 React、Vue、Angular 等框架的最佳实践和选择器优先级说明
  2. Vitest 覆盖率配置指南  — Vitest 官方的 v8/istanbul 覆盖率配置文档,包含阈值设置和报告生成
  3. Stryker Mutator  — JavaScript/TypeScript 变异测试框架,用于验证测试套件的真实检测能力
  4. fast-check 文档  — JavaScript/TypeScript PBT 框架的完整文档,包含生成器设计和收缩策略
  5. Hypothesis 文档  — Python PBT 框架的官方文档,包含状态机测试和数据库集成
  6. Testcontainers  — 容器化集成测试框架,支持 Java、Node.js、Python、Go、Rust 等语言
  7. Codecov  — 覆盖率分析平台,提供 PR 级别的覆盖率变化追踪和质量门禁
  8. Kent C. Dodds — Testing JavaScript  — JavaScript 测试的系统化课程,覆盖单元测试、集成测试、E2E 测试和测试策略
  9. Google Testing Blog  — Google 测试团队的博客,包含大规模测试策略、不稳定测试治理等实践经验
  10. Kiro Steering 文档  — Kiro 的 Steering 规则和 Spec 驱动开发文档,包含测试集成的最佳实践

参考来源


📖 返回 总览与导航 | 上一节:31d-AI辅助PBT | 下一节:32a-AI辅助DevOps概览

Last updated on