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 在生成测试代码时面临独特的挑战,与生成业务代码的问题截然不同:
- 测试目的理解偏差:AI 倾向于生成”让测试通过”的代码,而非”验证行为正确”的代码
- Mock 滥用:AI 默认 Mock 一切外部依赖,导致测试只验证 Mock 的行为而非真实逻辑
- 断言质量低:AI 生成的断言往往过于宽泛(如只检查”不为 null”)或过于具体(如检查完整 JSON 字符串)
- 复制粘贴倾向:AI 倾向于复制已有测试结构,仅修改少量参数,导致大量冗余测试
- 异步处理不当:AI 经常遗漏 await、使用固定延时而非动态等待,导致不稳定测试
- 边界值盲区:AI 倾向于测试”正常路径”,忽略空值、极端值、并发等边界场景
- 测试隔离不足:AI 生成的测试可能共享状态,导致执行顺序影响结果
- 覆盖率虚高:AI 可以轻松生成高覆盖率但低质量的测试,给团队虚假的安全感
1.2 工具推荐
| 工具 | 用途 | 价格 | 适用场景 |
|---|---|---|---|
| Claude Code | Agentic 编码,CLAUDE.md 测试规则 | 按 token 计费(Max $100/月起) | 复杂项目的测试生成与审查 |
| Kiro | Spec-Driven 测试,分层 Steering | 免费(预览期) | 需求→测试的全链路追溯 |
| Cursor | AI IDE,.cursorrules 测试规则 | 免费 / Pro $20/月 | 日常测试编写 |
| Vitest | 现代 JS/TS 测试框架 | 免费(开源) | Vite 项目的单元/集成测试 |
| Jest | 成熟的 JS/TS 测试框架 | 免费(开源) | React/Node.js 项目测试 |
| Pytest | Python 测试框架 | 免费(开源) | Python 项目全类型测试 |
| fast-check | JS/TS Property-Based Testing | 免费(MIT) | 属性测试和模糊测试 |
| Istanbul / c8 | JS/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 Code | CLAUDE.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 生成的测试进行系统审查,发现以下问题分布:
| 反模式 | 受影响测试数 | 占比 | 严重程度 |
|---|---|---|---|
| 过度 Mock | 42 | 27% | 🔴 高 |
| 断言不足 | 58 | 37% | 🔴 高 |
| 脆弱测试 | 12 | 8% | 🟡 中 |
| 测试实现细节 | 23 | 15% | 🟡 中 |
| 缺少边界测试 | 31 | 20% | 🟡 中 |
| 复制粘贴 | 18 | 12% | 🟢 低 |
| 异步问题 | 8 | 5% | 🔴 高 |
| 不稳定测试 | 11 | 7% | 🔴 极高 |
注:一个测试可能同时存在多个问题。
第 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%(核心改进) |
| 测试数量 | 156 | 98 | ↓ 37%(移除冗余测试) |
| CI 通过率 | 85% | 99.2% | ↑ 14.2% |
| 平均运行时间 | 45s | 32s | ↓ 29% |
关键发现:
- 行覆盖率下降了 4%,但变异测试分数翻倍——说明之前的高覆盖率是虚假的
- 测试数量减少了 37%,但检测能力大幅提升
- CI 通过率从 85% 提升到 99.2%,几乎消除了不稳定测试
- 运行时间减少 29%,因为移除了不必要的 Mock 设置和冗余测试
避坑指南
❌ 常见错误
-
追求 100% 覆盖率
- 问题:为了达到 100% 覆盖率而生成大量低质量测试,包括测试 getter/setter、配置文件、类型定义等
- 正确做法:设定合理的覆盖率目标(80% 行覆盖率 + 75% 分支覆盖率),重点关注业务核心逻辑的覆盖质量
-
用 Mock 让测试通过
- 问题:当测试失败时,通过添加更多 Mock 来”修复”测试,而非修复被测代码
- 正确做法:测试失败时先分析是代码 bug 还是测试问题,Mock 只用于隔离外部依赖
-
忽略变异测试
- 问题:只看行覆盖率,不验证测试是否真正能检测到代码变更
- 正确做法:定期运行变异测试(Stryker/mutmut),变异分数 ≥ 70% 才说明测试有效
-
Snapshot 测试滥用
- 问题:对 UI 组件使用 snapshot 测试,任何 UI 变更都导致 snapshot 更新,团队习惯性地
--updateSnapshot - 正确做法:用行为测试替代 snapshot 测试,只在测试序列化格式稳定性时使用 snapshot
- 问题:对 UI 组件使用 snapshot 测试,任何 UI 变更都导致 snapshot 更新,团队习惯性地
-
测试代码不做代码审查
- 问题:业务代码严格审查,但测试代码直接合并,导致低质量测试积累
- 正确做法:测试代码与业务代码同等审查标准,使用测试审查清单
-
AI 生成测试后不验证
- 问题:AI 生成的测试直接提交,不检查断言质量和覆盖范围
- 正确做法:每次 AI 生成测试后,运行变异测试验证质量,使用审查清单检查
-
不稳定测试用重试”修复”
- 问题:在 CI 中添加自动重试(retry: 3)来掩盖不稳定测试
- 正确做法:找到不稳定的根因(时序、共享状态、外部依赖)并修复
-
测试和代码由同一个 AI 对话生成
- 问题:在同一个 AI 对话中生成代码和测试,AI 可能让测试”配合”代码而非独立验证
- 正确做法:代码和测试在不同的 AI 对话中生成,或先写测试再生成代码(TDD 模式)
✅ 最佳实践
- Steering 规则先行:在项目开始时就配置测试 Steering 规则,而非事后补救
- 变异测试作为质量门:在 CI 中集成变异测试,变异分数低于阈值则阻止合并
- TDD 模式与 AI 结合:先让 AI 从需求生成测试,再让 AI 生成通过测试的代码
- PBT 补充边界覆盖:对核心业务逻辑使用 Property-Based Testing 自动探索边界值
- 测试金字塔平衡:单元测试 70% + 集成测试 20% + E2E 测试 10%
- 定期测试健康检查:每月运行一次完整的测试质量审查(覆盖率 + 变异测试 + 不稳定性检测)
- 测试代码也要重构:当测试变得难以维护时,像重构业务代码一样重构测试
- Spec 驱动的测试追溯:每个测试关联到需求文档的验收标准,确保需求变更时测试同步更新
相关资源与延伸阅读
- Testing Library 官方文档 — 行为驱动测试的权威指南,包含 React、Vue、Angular 等框架的最佳实践和选择器优先级说明
- Vitest 覆盖率配置指南 — Vitest 官方的 v8/istanbul 覆盖率配置文档,包含阈值设置和报告生成
- Stryker Mutator — JavaScript/TypeScript 变异测试框架,用于验证测试套件的真实检测能力
- fast-check 文档 — JavaScript/TypeScript PBT 框架的完整文档,包含生成器设计和收缩策略
- Hypothesis 文档 — Python PBT 框架的官方文档,包含状态机测试和数据库集成
- Testcontainers — 容器化集成测试框架,支持 Java、Node.js、Python、Go、Rust 等语言
- Codecov — 覆盖率分析平台,提供 PR 级别的覆盖率变化追踪和质量门禁
- Kent C. Dodds — Testing JavaScript — JavaScript 测试的系统化课程,覆盖单元测试、集成测试、E2E 测试和测试策略
- Google Testing Blog — Google 测试团队的博客,包含大规模测试策略、不稳定测试治理等实践经验
- Kiro Steering 文档 — Kiro 的 Steering 规则和 Spec 驱动开发文档,包含测试集成的最佳实践
参考来源
- Autonoma — How to Reduce Test Flakiness: Best Practices and Solutions (2025)
- TestRigor — Software Testing Anti-Patterns and Ways To Avoid Them (2025)
- V2 Solutions — AI-Generated Test Coverage Is Misleading Your QA Strategy (2025)
- SoftwareSeni — Understanding Anti-Patterns and Quality Degradation in AI-Generated Code (2025)
- TestCollab — AI in Software Testing: Practical Use Cases, Risks, and Adoption Roadmap (2025)
- Mergify — The Ultimate Guide to Building Reliable Test Automation (2025)
- CodeAnt — 18 Best Code & Test Coverage Tools for DevOps in 2026 (2025)
- Qalogy — Top Automation Anti-Patterns to Avoid (With Real Examples) (2025)
- Vitest — Coverage Configuration (2025)
- Cursor Blog — How Stripe rolled out a consistent Cursor experience for 3,000 engineers (2025)
📖 返回 总览与导航 | 上一节:31d-AI辅助PBT | 下一节:32a-AI辅助DevOps概览