测试原则
语言特定参考
有关特定语言的测试模式,还请阅读:
- TypeScript (Vitest/RTL/Playwright):references/typescript.md
核心测试哲学
- 测试是一等代码:保持测试质量与生产代码相同
- 快速反馈:测试应快速运行并提供即时反馈
- 可靠性:测试应具有确定性和可重现性
- 独立性:每个测试应独立运行
TDD 流程 [所有代码变更强制要求]
每个代码变更都要执行此流程:
红阶段
步骤 1:编写定义预期行为的测试 步骤 2:运行测试 步骤 3:确认测试 FAILS(如果通过,则测试错误)
绿阶段
步骤 1:编写最简代码使测试通过 步骤 2:运行测试 步骤 3:确认测试 PASSES
重构阶段
步骤 1:改进代码质量(消除重复,改善命名) 步骤 2:运行测试 步骤 3:确认测试 STILL PASSES
验证阶段 [强制要求 - 要求零错误]
步骤 1:执行你语言/项目的所有质量检查命令 步骤 2:修复所有错误,直到所有命令以零错误通过 步骤 3:确认没有回归
强制执行:任何质量检查失败都无法进入下一阶段
TDD 例外情况(不需要 TDD)
- 纯配置文件
- 仅文档
- 紧急修复(但修复后立即添加测试)
- 探索性原型(在合并前丢弃或用测试重写)
- 构建/部署脚本(除非包含业务逻辑)
质量要求 [强制要求]
覆盖率
- 将覆盖率视为发现未测试区域的诊断信号,而非目标。目标会被操控成琐碎的测试。
- 集中测试关键路径、业务逻辑以及回归会产生影响的那些行为。
- 优先考虑有意义的断言,而非覆盖率数字。任何所需的阈值来自项目的 CI、任务文件、工作计划或设计文档。
测试特征
所有测试必须:
- 独立:测试之间无依赖
- 可重现:相同输入始终产生相同输出
- 快速:在项目可接受的反馈窗口内完成完整测试套件,并标记那些显著减慢本地迭代或 CI 的套件
- 自我检查:无需手动验证即可明确通过/失败
- 及时:在代码编写后立即编写测试
强制执行:测试若不符合上述任何特征,必须立即修复
测试类型
单元测试
目的:隔离测试单个组件
特征:
- 测试单个函数、方法或类
- 快速执行(毫秒级)
- 无外部依赖
- 模拟外部服务
- 测试套件中的主体
集成测试
目的:测试组件之间的交互
特征:
- 一起测试多个组件
- 可能涉及数据库、文件系统或 API
- 比单元测试慢
- 验证模块之间的契约
- 测试套件中占较小部分
端到端 (E2E) 测试
目的:从用户角度测试完整工作流
特征:
- 测试整个应用程序栈
- 模拟真实用户交互
- 最慢的测试类型
- 数量最少
- 信心水平最高
测试金字塔
遵循测试金字塔结构:
/\ <- 少量 E2E 测试(高信心,慢)
/ \
/ \ <- 一些集成测试(中等信心,中等速度)
/ \
/________\ <- 大量单元测试(快,基础)
测试设计原则
AAA 模式 (Arrange-Act-Assert)
将每个测试结构化为三个清晰阶段:
// Arrange:准备测试数据和条件
user = createTestUser()
validator = createValidator()
// Act:执行被测试的代码
result = validator.validate(user)
// Assert:验证预期结果
assert(result.isValid == true)
每个概念一个断言
- 每个测试用例测试一种行为
- 如果测试单一概念,可以有多个断言
- 将无关的断言拆分为单独的测试——一个测试必须验证一种行为
描述性测试名称
测试名称应清晰描述:
- 正在测试什么
- 在什么条件下
- 预期结果是什么
推荐格式:"should [expected behavior] when [condition]"
测试独立性
隔离要求
- 每个测试创建自己的测试数据
- 不依赖执行顺序
- 清理自己的状态
- 隔离运行时能通过
设置和清理
- 使用设置钩子准备测试环境
- 使用清理钩子释放资源
- 将设置范围限定为被测试行为所需的数据、依赖项和夹具
- 确保即使测试失败,清理也会运行
模拟和测试替身
何时使用模拟
- 模拟外部依赖:API、数据库、文件系统
- 模拟慢操作:网络调用、重计算
- 模拟不可预测行为:随机值、当前时间
- 模拟不可用服务:第三方服务
模拟原则 [强制要求]
- 在边界上模拟,而不是内部——内部实用程序使用真实实现
- 将每个模拟限制为测试需要控制或观察的行为
- 在相关时验证模拟期望
- 对不受控制的外部库/框架使用适配器
测试替身的类型
- 存根:返回预定值
- 模拟:验证其被正确调用
- 间谍:记录有关调用的信息
- 伪造:简化的有效实现
- 哑元:传递但从未使用
数据层测试
数据访问的模拟限制
模拟验证调用模式,但不验证模式正确性、查询正确性或存储约束。 模拟可能遗漏的问题示例:
- 模式漂移
- 列或字段不匹配
- 错误的连接、过滤器或聚合
- 迁移不兼容
何时需要真实数据层验证
在测试以下内容时,使用真实或接近生产的数据访问验证:
- 仓库或 DAO 的实现
- ORM 映射
- 查询构建器或原始 SQL
- 依赖于约束或模式形状的持久化行为
环境选项
为项目环境选择最实用的选项:
- 容器化数据库
- 专用测试数据库
- 具有文档化限制的内存数据库
- 适配器支持的本地测试工具
设计一致性
当设计文档包含 Test Boundaries 时,将其作为决定哪些依赖保持真实、哪些边界被隔离的基准。
测试质量实践 [强制要求]
保持测试活跃
- 修复或删除失败的测试:立即解决失败
- 移除注释掉的测试:修复它们或完全删除
- 保持测试运行:损坏的测试会迅速失去价值
- 维护测试套件:根据需要重构测试
测试代码质量
- 应用与生产代码相同的标准
- 使用描述性变量名
- 提取测试辅助程序以减少重复
- 保持测试可读性和可维护性
测试辅助程序和工具
- 创建可重用的测试数据构建器
- 将公共设置提取到辅助函数中
- 为复杂场景构建测试工具
- 在测试文件之间适当共享辅助程序
测试内容
关注行为
测试可观察行为,而非实现:
- 好:测试函数返回预期输出
- 好:测试调用了正确的 API 端点
- 坏:测试内部变量是否被设置
- 坏:测试私有方法调用的顺序
测试边缘情况
始终测试:
- 边界条件:最小值/最大值、空集合
- 错误情况:无效输入、空值、缺失数据
- 边缘情况:特殊字符、极值
- 快乐路径:正常、预期使用
测试质量标准 [强制要求]
- 字面量期望:在断言中使用硬编码的字面量值——预期值 ≠ 模拟返回值(实现处理数据)
- 结果验证:断言返回值和状态,而不是调用顺序
- 有意义的断言:每个测试必须至少有一个断言——没有断言的测试提供零价值
- 仅模拟外部 I/O:模拟数据库/API/文件系统,使用真实的内部实用程序
- 边界覆盖:在快乐路径中包含空/零/最大/错误情况
强制执行:违反任何标准的测试必须重写
验证要求 [验证阶段强制要求]
提交前清单
☐ 所有测试通过 ☐ 没有测试被跳过或注释掉 ☐ 测试中没有遗留调试代码 ☐ 当项目、任务文件、工作计划或设计文档定义了覆盖率阈值时,通过阈值 ☐ 测试在合理时间内运行
零容忍政策
- 零失败测试:立即修复
- 零跳过测试:删除或修复
- 零不稳定测试:使其具有确定性
- 零慢测试:优化或拆分
强制执行:如果任何质量检查失败,无法继续完成任务
测试组织
文件结构
- 镜像生产结构:测试遵循代码组织方式
- 清晰的命名约定:遵循项目的测试文件模式
- 逻辑分组:将相关测试分组在一起
- 分离测试类型:单元测试、集成测试、端到端测试放在不同目录
测试套件组织
tests/
├── unit/ # 快速、隔离的单元测试
├── integration/ # 集成测试
├── e2e/ # 端到端测试
├── fixtures/ # 测试数据和夹具
└── helpers/ # 共享的测试实用程序
性能考虑
测试速度
- 单元测试:每个 < 100 毫秒
- 集成测试:每个 < 1 秒
- 完整套件:应频繁运行(< 10 分钟)
常见反模式
立即检测并消除这些模式:
- 什么都不测试的测试(始终通过)
- 依赖执行顺序的测试
- 依赖外部状态的测试
- 包含复杂逻辑的测试(需要自己的测试的测试)
- 测试实现细节而非可观察行为
- 过度模拟(仅在边界处模拟,使用真实内部)
- 测试代码重复
不稳定测试
消除间歇性失败的测试:
- 移除时间依赖
- 使用确定性数据而非随机值
- 确保适当的清理
- 修复竞态条件
- 使所有测试具有确定性
回归测试
- 为每个错误修复添加测试
- 维护全面的测试套件
- 定期运行完整套件
- 仅在覆盖的行为不再存在,或同一行为被正确层级上更强的测试覆盖时才删除测试
遗留代码
- 在重构前添加表征测试
- 首先测试现有行为
- 逐步改进覆盖率
- 自信地重构