名称: 测试策略 描述: 全面测试策略指导,包括测试金字塔设计、覆盖率目标、测试分类、不稳定测试诊断、测试基础设施架构和基于风险的优先级排序。吸收了已淘汰高级质量保证工程师的专业知识。适用于规划测试方法、设置测试基础设施、优化测试套件、诊断不稳定测试或跨领域设计测试架构(API、数据管道、机器学习模型、基础设施)。触发关键词:测试策略、测试金字塔、测试计划、测试什么、如何测试、测试架构、测试基础设施、覆盖率目标、测试组织、CI/CD测试、测试优先级、测试方法、不稳定测试、测试优化、测试并行化、API测试策略、数据管道测试、机器学习模型测试、基础设施测试。
测试策略
概述
测试策略定义了项目测试的方法,平衡彻底性与效率。设计良好的策略确保关键功能被覆盖,同时避免过度测试琐碎代码。本技能涵盖测试金字塔、覆盖率指标、测试分类以及与CI/CD流水线的集成。
指令
1. 设计测试金字塔
以适当比例分层结构化测试:
/\
/ \ 端到端测试 (5-10%)
/----\ - 关键用户旅程
/ \ - 跨系统集成
/--------\ 集成测试 (15-25%)
/ \ - API合约
/------------\ - 数据库交互
/ \ - 服务边界
/----------------\ 单元测试 (65-80%)
- 业务逻辑
- 纯函数
- 边界情况
推荐比例:
- 单元测试:测试套件的65-80%
- 集成测试:15-25%
- 端到端测试:5-10%
2. 设置覆盖率目标
按组件类型的覆盖率目标:
| 组件类型 | 行覆盖率 | 分支覆盖率 | 注释 |
|---|---|---|---|
| 业务逻辑 | 90%+ | 85%+ | 关键路径完全覆盖 |
| API处理器 | 80%+ | 75%+ | 所有端点测试 |
| 工具函数 | 95%+ | 90%+ | 纯函数易于测试 |
| UI组件 | 70%+ | 60%+ | 关注行为而非标记 |
| 基础设施 | 60%+ | 50%+ | 首选集成测试 |
应避免的覆盖率反模式:
- 为覆盖率而追求100%
- 测试无逻辑的getter/setter
- 测试框架或库代码
- 编写不验证行为的测试
3. 决定测试什么与不测试什么
始终测试:
- 业务逻辑和领域规则
- 输入验证和错误处理
- 安全敏感操作
- 数据转换
- 状态转换
- 边界情况和边界条件
- 来自错误修复的回归场景
考虑不测试:
- 简单传递函数
- 框架生成的代码
- 第三方库内部
- 琐碎的getter/setter
- 配置常量
- 日志语句(除非关键)
测试异味检测:
// 差:测试琐碎代码
test("getter返回值", () => {
const user = new User("John");
expect(user.getName()).toBe("John");
});
// 好:测试有意义的行为
test("用户不能将名称更改为空字符串", () => {
const user = new User("John");
expect(() => user.setName("")).toThrow(ValidationError);
});
4. 分类和组织测试
目录结构:
tests/
├── unit/
│ ├── services/
│ ├── models/
│ └── utils/
├── integration/
│ ├── api/
│ ├── database/
│ └── external-services/
├── e2e/
│ ├── flows/
│ └── pages/
├── fixtures/
│ ├── factories/
│ └── mocks/
└── helpers/
├── setup.ts
└── assertions.ts
测试标记系统:
// Jest带标记示例
describe("[unit][fast] UserService", () => {});
describe("[integration][slow] DatabaseRepository", () => {});
describe("[e2e][critical] CheckoutFlow", () => {});
// 运行特定类别
// npm test -- --grep="\[unit\]"
// npm test -- --grep="\[critical\]"
命名约定:
[组件名称].[场景].[预期结果].test.ts
示例:
UserService.createUser.returnsNewUser.test.ts
PaymentProcessor.invalidCard.throwsPaymentError.test.ts
5. 与CI/CD集成
流水线阶段配置:
# .github/workflows/test.yml
名称: 测试流水线
触发: [push, pull_request]
作业:
单元测试:
运行于: ubuntu-latest
步骤:
- 使用: actions/checkout@v4
- 名称: 运行单元测试
运行: npm test -- --grep="\[unit\]" --coverage
- 名称: 上传覆盖率
使用: codecov/codecov-action@v3
集成测试:
运行于: ubuntu-latest
需要: 单元测试
服务:
postgres:
镜像: postgres:15
环境:
POSTGRES_PASSWORD: test
步骤:
- 使用: actions/checkout@v4
- 名称: 运行集成测试
运行: npm test -- --grep="\[integration\]"
端到端测试:
运行于: ubuntu-latest
需要: 集成测试
步骤:
- 使用: actions/checkout@v4
- 名称: 运行端到端测试
运行: npm run test:e2e
CI测试优化:
- 首先运行单元测试(快速反馈)
- 并行化测试套件
- 缓存依赖项和构建工件
- 对大型套件使用测试分片
- 关键测试快速失败
6. 基于风险的测试优先级排序
优先级风险矩阵:
| 影响 ↓ / 可能性 → | 低 | 中 | 高 |
|---|---|---|---|
| 高 | 中优先级 | 高优先级 | 关键 |
| 中 | 低优先级 | 中优先级 | 高优先级 |
| 低 | 跳过/手动 | 低优先级 | 中优先级 |
考虑的风险因素:
- 业务影响: 收入、用户信任、法律合规性
- 复杂性: 代码复杂性、集成点
- 变更频率: 活跃开发区域
- 历史错误: 有错误历史的组件
- 依赖项: 关键外部服务
优先级测试类别:
-
关键 (P0): 每次提交运行
- 认证/授权
- 支付处理
- 数据完整性
-
高 (P1): PR合并时运行
- 核心业务工作流
- API合约测试
-
中 (P2): 每晚运行
- 边界情况
- 性能测试
-
低 (P3): 每周运行
- 向后兼容性
- 弃用功能覆盖
7. 领域特定测试策略
API测试策略
测试层级:
-
合约测试 (P0)
- 请求/响应模式验证
- 所有端点的HTTP状态码
- 错误响应格式
- 认证/授权规则
-
业务逻辑测试 (P0)
- 有效输入处理
- 业务规则执行
- 通过API调用的状态转换
-
集成测试 (P1)
- 通过API的数据库操作
- 外部服务集成
- 事务回滚场景
-
性能测试 (P2)
- 负载下的响应时间
- 并发请求处理
- 速率限制行为
API测试组织:
tests/api/
├── contracts/ # 模式验证测试
├── endpoints/ # 每端点行为测试
├── auth/ # 认证流程
├── integration/ # 跨服务场景
└── performance/ # 负载和压力测试
数据管道测试策略
测试重点领域:
-
数据质量测试 (P0)
- 每阶段模式验证
- 数据类型正确性
- 空值/缺失值处理
- 重复检测
-
转换测试 (P0)
- 输入 → 输出正确性
- 边界情况处理
- 数据丢失检测
- 聚合准确性
-
集成测试 (P1)
- 源提取正确性
- 汇加载验证
- 幂等性检查
- 故障恢复
-
性能测试 (P2)
- 处理吞吐量
- 大数据集内存使用
- 分区处理
数据管道测试模式:
def test_user_data_transformation():
# 安排:创建测试输入数据
raw_input = create_test_dataset(
rows=1000,
include_nulls=True,
include_duplicates=True
)
# 执行:运行转换
result = transform_user_data(raw_input)
# 断言:验证输出质量
assert_no_nulls(result, required_fields=["user_id", "email"])
assert_no_duplicates(result, key="user_id")
assert_schema_matches(result, UserSchema)
assert len(result) == expected_output_count(raw_input)
机器学习模型测试策略
测试层级:
-
数据验证测试 (P0)
- 特征模式验证
- 标签分布检查
- 数据泄漏检测
- 训练/测试分割正确性
-
模型行为测试 (P0)
- 已知示例预测
- 不变性测试(如大小写不敏感文本)
- 方向性期望测试
- 边界条件处理
-
模型质量测试 (P1)
- 准确率/精确率/召回率阈值
- 跨组公平性指标
- 边界情况性能
- 与基线回归检测
-
集成测试 (P1)
- 模型加载和服务
- 预测API合约
- 特征工程流水线
- 模型版本控制
机器学习测试示例:
def test_sentiment_model_invariance():
"""模型应大小写不敏感"""
model = load_sentiment_model()
test_cases = [
("This is GREAT!", "This is great!"),
("TERRIBLE service", "terrible service"),
]
for text1, text2 in test_cases:
pred1 = model.predict(text1)
pred2 = model.predict(text2)
assert pred1 == pred2, f"检测到大小写敏感性:{text1} vs {text2}"
基础设施测试策略
测试重点:
-
基础设施即代码测试 (P0)
- 语法验证(terraform validate)
- 安全策略检查
- 资源命名约定
- 成本估算验证
-
部署测试 (P1)
- 部署后冒烟测试
- 健康检查端点
- 配置验证
- 回滚程序
-
弹性测试 (P2)
- 服务重启处理
- 网络分区恢复
- 资源耗尽场景
- 混沌工程测试
-
可观察性测试 (P1)
- 指标收集验证
- 日志聚合正确性
- 警报规则验证
- 仪表板功能
基础设施测试模式:
# terraform测试示例
run "verify_security_group_rules" {
command = plan
assert {
condition = length([for rule in aws_security_group.main.ingress : rule if rule.cidr_blocks[0] == "0.0.0.0/0"]) == 0
error_message = "安全组不应允许来自0.0.0.0/0的入站流量"
}
}
8. 不稳定测试诊断与预防
不稳定性常见原因:
| 原因 | 症状 | 解决方案 |
|---|---|---|
| 竞态条件 | 时间上间歇性失败 | 添加适当同步 |
| 异步操作 | 因“元素未找到”失败 | 使用显式等待,而非睡眠 |
| 共享状态 | 与其他测试一起运行时失败 | 隔离测试数据,重置状态 |
| 外部依赖 | 服务不可用时失败 | 模拟外部调用,使用测试替身 |
| 时间依赖逻辑 | 在特定时间/日期失败 | 注入时间,使用假时钟 |
| 资源清理 | 特定测试顺序后失败 | 确保清理始终运行 |
| 非确定性数据 | 随机数据变化失败 | 使用固定种子,确定性生成器 |
| 环境差异 | CI失败但本地通过 | 容器化测试环境 |
| 超时不足 | 负载/慢机器下失败 | 使超时可配置 |
| 并行执行竞态 | 仅并行化时失败 | 使用每个测试的唯一标识符 |
不稳定测试诊断工作流:
1. 本地复现
├─ 运行测试100次:`for i in {1..100}; do npm test -- TestName || break; done`
├─ 用不同种子运行:`npm test -- --seed=$RANDOM`
└─ 并行运行:`npm test -- --maxWorkers=4`
2. 识别模式
├─ 总是在同一点失败?→ 逻辑错误,非不稳定
├─ 负载下失败?→ 时间/资源问题
├─ 与其他测试一起失败?→ 共享状态污染
└─ 特定数据失败?→ 数据依赖错误
3. 工具化测试
├─ 添加详细日志
├─ 捕获时间信息
├─ 记录测试环境状态
└─ 保存失败工件(截图、日志)
4. 修复根本原因
├─ 消除竞态条件
├─ 添加适当同步
├─ 隔离测试状态
└─ 模拟外部依赖
5. 验证修复
├─ 运行修复测试1000次
├─ 在CI中运行10次
└─ 监控一周
不稳定测试预防清单:
- [ ] 测试使用确定性测试数据(固定种子,无random())
- [ ] 异步操作使用显式等待(非setTimeout/sleep)
- [ ] 测试创建唯一资源(名称/ID中的UUID)
- [ ] 清理始终运行(try/finally,afterEach钩子)
- [ ] 无硬编码时间假设(sleep(100)是代码异味)
- [ ] 外部服务被模拟或使用测试替身
- [ ] 时间依赖逻辑使用注入/假时钟
- [ ] 测试不依赖执行顺序
- [ ] 共享状态在测试间重置
- [ ] 测试环境可重现(容器化)
示例:修复不稳定测试
// 不稳定:异步操作竞态条件
test("用户配置文件加载", async () => {
renderUserProfile(userId);
// 竞态:配置文件可能尚未加载
expect(screen.getByText("John Doe")).toBeInTheDocument();
});
// 修复:适当的异步处理
test("用户配置文件加载", async () => {
renderUserProfile(userId);
// 等待异步操作完成
const userName = await screen.findByText("John Doe");
expect(userName).toBeInTheDocument();
});
// 不稳定:共享状态污染
test("以默认角色创建用户", () => {
const user = createUser({ name: "Alice" });
expect(user.role).toBe("user"); // 如果先前测试修改了默认值则失败
});
// 修复:隔离状态
test("以默认角色创建用户", () => {
resetDefaultRole(); // 确保干净状态
const user = createUser({ name: "Alice" });
expect(user.role).toBe("user");
});
// 不稳定:时间依赖逻辑
test("1小时后会话过期", () => {
const session = createSession();
// 不稳定:依赖当前时间
expect(session.expiresAt).toBe(Date.now() + 3600000);
});
// 修复:注入时间依赖
test("1小时后会话过期", () => {
const mockClock = installFakeClock();
mockClock.setTime(new Date("2024-01-01T12:00:00Z"));
const session = createSession();
expect(session.expiresAt).toBe(new Date("2024-01-01T13:00:00Z").getTime());
mockClock.uninstall();
});
9. 测试基础设施架构
测试环境管理:
# docker-compose.test.yml
版本: "3.8"
服务:
测试数据库:
镜像: postgres:15
环境:
POSTGRES_DB: test_db
POSTGRES_USER: test_user
POSTGRES_PASSWORD: test_pass
端口:
- "5433:5432"
tmpfs:
- /var/lib/postgresql/data # 内存中以提高速度
测试Redis:
镜像: redis:7-alpine
端口:
- "6380:6379"
测试应用:
构建: .
环境:
DATABASE_URL: postgres://test_user:test_pass@测试数据库:5432/test_db
REDIS_URL: redis://测试Redis:6379
依赖于:
- 测试数据库
- 测试Redis
测试数据管理:
// 测试数据的工厂模式
class UserFactory {
private sequence = 0;
create(overrides?: Partial<User>): User {
return {
id: overrides?.id ?? `user-${this.sequence++}`,
email: overrides?.email ?? `user${this.sequence}@test.com`,
name: overrides?.name ?? `Test User ${this.sequence}`,
role: overrides?.role ?? "user",
createdAt: overrides?.createdAt ?? new Date(),
};
}
createBatch(count: number, overrides?: Partial<User>): User[] {
return Array.from({ length: count }, () => this.create(overrides));
}
}
// 使用确保每个测试有唯一数据
test("用户搜索工作", () => {
const factory = new UserFactory();
const users = factory.createBatch(10);
// 每个测试获取唯一用户,无冲突
});
测试并行化策略:
| 策略 | 何时使用 | 配置 |
|---|---|---|
| 文件级并行 | 不同文件中测试独立 | Jest: --maxWorkers=4 |
| 每工作器数据库 | 测试需要数据库隔离 | Postgres: 为每个工作器创建模式 |
| 测试分片 | CI中有多个机器 | 按分片拆分测试:--shard=1/4 |
| 测试优先级排序 | 希望快速反馈 | 首先运行快速测试,慢测试并行运行 |
| 智能测试选择 | 仅运行受影响测试 | 使用依赖图选择更改的测试 |
示例:并行测试配置
// 带有并行优化的jest.config.js
module.exports = {
maxWorkers: process.env.CI ? "50%" : "75%", // CI中保守
testTimeout: 30000, // CI中更长的超时
// 首先运行快速测试
testSequencer: "./custom-sequencer.js",
// 每个工作器的数据库隔离
globalSetup: "./tests/setup/create-test-dbs.js",
globalTeardown: "./tests/setup/drop-test-dbs.js",
// CI中分片测试
shard: process.env.CI_NODE_INDEX
? `${process.env.CI_NODE_INDEX}/${process.env.CI_NODE_TOTAL}`
: undefined,
};
测试优化技术:
-
减少测试启动时间
- 缓存编译代码
- 懒加载测试依赖项
- 对单元测试使用内存数据库
-
优化测试执行
- 批量数据库操作
- 重用昂贵固定装置(连接、容器)
- 为聚焦测试跳过不必要的设置
-
安全并行化
- 每个测试的唯一标识符(UUID)
- 每个工作器的独立数据库模式
- 避免共享文件系统访问
-
智能测试选择
- 开发期间仅运行受影响测试
- 使用覆盖率映射确定受影响测试
- 缓存未更改代码的测试结果
# 仅运行受更改影响的测试
npm test -- --changedSince=origin/main
# 运行特定模块及其依赖项的测试
npm test -- --selectProjects=user-service --testPathPattern=user
# 带有智能重新运行的监视模式
npm test -- --watch --changedSince=HEAD
最佳实践
-
测试行为,而非实现
- 测试应验证结果,而非内部机制
- 如果行为未变,重构不应破坏测试
-
保持测试独立
- 测试间无共享可变状态
- 每个测试设置自己的上下文
- 测试可以任何顺序运行
-
适当使用测试替身
- 用于提供测试数据的桩
- 用于验证交互的模拟
- 用于复杂依赖的假对象
- 可行时使用真实实现
-
维护测试质量
- 对测试应用相同的代码质量标准
- 重构测试代码以提高可读性
- 及时移除过时测试
-
快速反馈循环
- 优化快速本地测试运行
- 开发期间使用监视模式
- CI中优先快速测试
-
记录测试意图
- 清晰的测试名称描述行为
- 为非明显设置添加注释
- 链接测试到需求/工单
示例
示例:功能测试策略文档
# 功能:用户注册
## 风险评估
- 业务影响:高(用户获取)
- 复杂性:中(邮件验证、密码规则)
- 变更频率:低(稳定功能)
## 测试覆盖率计划
### 单元测试 (P0)
- [ ] 邮件格式验证
- [ ] 密码强度要求
- [ ] 用户名唯一性检查逻辑
- [ ] 配置文件数据清理
### 集成测试 (P1)
- [ ] 数据库用户创建
- [ ] 邮件服务集成
- [ ] 重复邮件处理
### 端到端测试 (P0)
- [ ] 顺利路径:完成注册流程
- [ ] 错误路径:重复邮件显示错误
## 覆盖率目标
- 行覆盖率:85%
- 分支覆盖率:80%
- 关键路径:100%
示例:测试组织配置
// jest.config.js
module.exports = {
projects: [
{
displayName: "单元",
testMatch: ["<rootDir>/tests/unit/**/*.test.ts"],
setupFilesAfterEnv: ["<rootDir>/tests/helpers/unit-setup.ts"],
},
{
displayName: "集成",
testMatch: ["<rootDir>/tests/integration/**/*.test.ts"],
setupFilesAfterEnv: ["<rootDir>/tests/helpers/integration-setup.ts"],
globalSetup: "<rootDir>/tests/helpers/db-setup.ts",
globalTeardown: "<rootDir>/tests/helpers/db-teardown.ts",
},
],
coverageThreshold: {
global: {
branches: 75,
functions: 80,
lines: 80,
statements: 80,
},
"./src/services/": {
branches: 90,
lines: 90,
},
},
};
示例:基于风险的测试选择脚本
// scripts/select-tests.ts
interface TestFile {
path: string;
priority: "P0" | "P1" | "P2" | "P3";
tags: string[];
}
function selectTestsForPipeline(
context: "commit" | "pr" | "nightly" | "weekly",
): TestFile[] {
const allTests = getTestManifest();
const priorityMap = {
commit: ["P0"],
pr: ["P0", "P1"],
nightly: ["P0", "P1", "P2"],
weekly: ["P0", "P1", "P2", "P3"],
};
return allTests.filter((test) =>
priorityMap[context].includes(test.priority),
);
}