mcpField
T
testing-best-practices

testing-best-practices

github.com/adewale测试与质量1270 次安装3 周前更新
代码仓库

关于 testing-best-practices

testing-best-practices 是用于编写和审查软件测试的技能,提供 Write、Assess、Upgrade 和 Detect 四种模式,并涵盖多种语言、框架与测试类型。

主要功能

  1. 01编写测试并先测后修
  2. 02评估测试套件质量
  3. 03强化薄弱或不稳定的测试
  4. 04发现跳过或误导性测试
  5. 05按语言和主题选择参考资料

适用场景

  1. 01为缺陷修复添加测试
  2. 02超越覆盖率指标审查测试套件
  3. 03改进不稳定或过度模拟的测试
  4. 04发现跳过的测试和覆盖缺口

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/adewale/testing-best-practices --skill testing-best-practices

安装后在 Claude Code 中输入/testing-best-practices确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
antipatterns.md
characterization-testing.md
combinatorial-testing.md
correctness-by-construction.md
deterministic-time.md
differential-testing.md
doc-sync-testing.md
exhaustive-testing.md
fuzzing.md
gate-integrity.md
go.md
golden-file-testing.md
mathematical-properties.md
mutation-testing.md
property-based-testing.md
python.md
rust.md
test-data-builders.md
test-types.md
typescript.md
vcr-cassettes.md

详细介绍

测试最佳实践

此技能包含四种模式:

  1. 编写 — 添加测试,或采用测试优先的方式修复缺陷/实现功能。
  2. 评估 — 评审现有测试套件的质量,而不只是测试覆盖率。
  3. 改进 — 加强薄弱、易出错或过度模拟的测试。
  4. 检测 — 查找被跳过、被篡改、虚假覆盖或容易遗漏问题的测试。

前 90 秒

在编写、评估或修改测试之前:

  1. 检测语言/框架:根据仓库文件和附近的测试确定。
  2. 阅读附近的测试:了解命名、测试夹具、构建器、断言风格和运行器命令。
  3. 找到最近的验证命令,再考虑范围更广的完整套件命令。
  4. 确定风险边界:纯逻辑、内部组件边界、外部 API、UI/用户流程、安全边界、恶意输入解析器或类型/架构不变量。
  5. 对变更和资产进行分类:行为变更还是结构调整/整理;可复用库还是临时探测;面向客户的规则还是内部辅助工具。
  6. 根据 references/test-types.md,选择最小且有用的测试层级。
  7. 根据下方矩阵,仅加载相关参考资料。

如果某种语言没有专门的参考资料,请遵循项目惯例和此处的通用原则。

参考矩阵

始终考虑:

  • 测试类型选择 → references/test-types.md
  • 反模式检测 → references/antipatterns.md

语言/框架参考:

  • Python / pytest / Hypothesis → references/python.md
  • TypeScript/JavaScript / Vitest/Jest/fast-check/Playwright → references/typescript.md
  • Go / testing / testing.F / Rapid → references/go.md
  • Rust → references/rust.md

对于基于属性的测试或覆盖率引导的模糊测试,也要加载匹配的语言参考:不同引擎在收集、发现和重放方面各不相同。如果此处未记录所选引擎,请遵循其项目文档,不要套用其他引擎的行为。

按触发条件选择专题参考:

  • 编写/审查属性测试、选择属性、生成器设计、有状态/基于模型的测试、种子和重放 → references/property-based-testing.md
  • 针对暴露/自定义解析器、原生或不安全解码器、易崩溃边界,或崩溃会禁用检查的安全工具,设计覆盖率引导的目标或测试活动 → references/fuzzing.md
  • 遗留代码/重构安全 → references/characterization-testing.md
  • 重新实现、移植、多语言 SDK、没有参考实现的自定义数据结构,或近似/概率/ANN/排序输出 → references/differential-testing.md
  • 复杂输出、快照、转换流水线、保存/加载或迁移往返 → references/golden-file-testing.md
  • 时间、计时器、计划任务、休眠、不稳定的时间测试、测试只能通过休眠才能访问的后台线程/异步工作 → references/deterministic-time.md
  • 外部 API、录制的真实响应、模拟数据漂移 → references/vcr-cassettes.md
  • CLI/插件/文档注册表同步,或文档和 Agent 指令中的验证声明 → references/doc-sync-testing.md
  • 变异测试、变异分数/存活变异、定期变异测试流程,或覆盖率很高却仍有漏洞 → references/mutation-testing.md。对于定期执行的流程,如果没有已完成的目标 CI 基线或已证明对某类故障有效,就不要安排;绝不要照搬绝对分数下限。
  • 有限状态空间或代价高昂的笛卡尔积 → references/exhaustive-testing.md
  • 大型配置/功能/运行时矩阵、成对替代方案或相互作用的因素 → references/combinatorial-testing.md。保留固定回归用例和精确的注册项;使用具名的可变强度组,并在删除前保留影子证据。
  • 算术/领域运算符/定律 → references/mathematical-properties.md
  • 测试夹具/构建器/断言辅助函数 → references/test-data-builders.md
  • 多层检查同一不变量、类型与测试的取舍、无效状态 → references/correctness-by-construction.md
  • CI/钩子/测试配置变更、门控或定期执行的层级、“此检查是否在运行且能否失败?”、部署验证 → references/gate-integrity.md

核心原则

通过公共契约测试行为

优秀的测试描述可观察到的行为:输出、状态转换、副作用、渲染的 UI、持久化数据、API 契约或不变量。避免编写会因不改变行为的重构而失效的测试,例如测试私有方法、偶然的调用顺序、精确的 SQL 字符串或大范围的模拟调用编排。

当用户提供示例、表格、错误报告、电子表格行或支持案例时,首先将其视为沟通材料,其次才是断言。保留业务语言,然后通过最窄且诚实的接缝运行这些示例:通常是领域/服务/API 契约,而不是为每一行业务规则都走一遍完整 UI 流程。UI/E2E 测试应证明接线正确且关键路径可用;规则表应直接测试领域层。

根据生命周期和改动类型调整测试投入

不要对所有产物采用相同的测试计划。可复用库、解析器、支付/身份验证/安全边界、迁移,以及将会重构的代码,都值得更强的回归/属性/契约测试覆盖。一次性探测、临时研究脚本或生成的探索性代码,可能只需要冒烟测试;如果用户接受其生命周期,也可以不写测试。

区分行为改动与结构改动。行为改动需要有能因新行为或变更后的行为而失败的测试。纯整理/重构应依托现有的全绿测试套件;如果没有这样的测试套件,应在重构前为需要保留的行为添加特征测试。

对于没有流水线的新服务,应从步行骨架开始:构建/部署/测试的最精简切片。将尚未完成的验收测试作为可见的进行中事项,并关联到故事/问题,而不要悄悄跳过。保持单元测试全绿;不要提交失败的单元测试来充当证明。

在可行时采用红-绿-重构

对于 Bug 修复和新行为,默认采用以下流程:

  1. 红 — 添加一个有针对性的测试,它会因当前 Bug/缺失行为而失败。
  2. 绿 — 实现能够通过测试的最小改动。
  3. 重构 — 在测试保持全绿的同时进行清理。

证据很重要。将红阶段证据(修复前的确切命令和失败输出)与绿阶段证据(修复后的通过命令/结果)分开。只有按顺序观察到两者,才能声称已完成 TDD/红-绿-重构。如果只有通过/绿阶段的运行结果,应说明已添加测试且现已通过,但未观察到/无法核实红阶段;不要仅凭只有绿阶段的日志,事后声称采用了 TDD。对于探索性原型、生成的代码或用户限定为“仅编写测试”的任务,应遵守范围并说明偏离之处。

采用 TDD 时,维护一份可见的测试列表。将新的边界情况想法先记入列表,不要在当前循环中追着它们实现。如果遇到阻碍,可降低难度:先写断言,拆出更小的子测试,为第三方 API 编写学习测试,或使用崩溃测试替身触发难以覆盖的错误路径。根据不确定性选择伪实现、三角法或显而易见的实现;绝不要让伪造的常量成为最终行为。

质量胜过覆盖率

覆盖率能显示运行过哪些代码,但不能证明能发现 bug。优先关注分支覆盖率,而非行覆盖率,并将覆盖率视为查找未测试路径的地图,而不是单独的质量门槛。

断言数量只是经验法则,不是铁律;切勿将其变成配额。基于示例的行为测试通常需要多个有意义的断言来验证结构、状态和反例。但属性测试、表格行或异常测试可能只需一个出色的判定依据。留意薄弱的单一断言,例如“非空”、toBeDefined()、toBeTruthy()、Assert.IsNotNull(result),或没有断言的日志记录。

对于清理器、验证器、过滤器、身份验证/安全检查和转换,在适用时验证两个方向:危险/无效内容会被拒绝或移除,安全/有效内容会被保留。

断言与被测行为相关的字段。对整个对象/整个结构进行相等性比较,会隐式断言所有无关字段,因此测试会因无关改动而失败,并陷入变更检测的疲劳循环(每次新增字段都要更新字面量)。仅在覆盖范围本身就是约定时,才比较整体状态——例如黄金文件和保存/加载往返测试(references/golden-file-testing.md)。优先使用仅凭失败消息就能着手调试的断言形式(能打印预期值与实际值的匹配器/流畅断言,而不是裸布尔值);当许多测试因迭代顺序等无关因素反复失败时,应改进断言用法(使用忽略顺序/结构化匹配器),而不是逐个修改测试。对于覆盖率,应针对新增/变更代码设定门槛,而不是设定整个仓库的统一数值;将未覆盖的部分视为评审信号。

每个自定义判定器、代码检查器、验证器、修复器或评估评分器,都需要一个已知良好的输入(应通过)和一个已知有问题的输入(应失败);保持输入精简,不要对每个有问题的输入都重新运行耗时的检查器。在与代码相同的改动中由同一方编写的黄金文件、审批样例或评估用例,只能用于描述现状,不能作为独立判定依据。

优先测试真实行为,而非模拟对象

按以下顺序优先选择:

  1. 真实的内存中/本地对象、临时目录、内存数据库、真实解析器。
  2. 对真实运行时的本地仿真(workerd/vitest-pool-workers、Node 中的 Pyodide、wrangler dev、从迁移文件加载的 SQLite)。
  3. 实现相同接口且能记录历史记录的专用伪造对象。
  4. 用于受控边界情况的确定性桩对象。
  5. 最后才考虑框架模拟对象。

模拟对象的预期值应取自真实服务(通过记录或本地仿真获取),而不是模拟对象作者的设想;如果能在进程内运行真实引擎,就删除模拟对象。

优先验证可见状态,而不是调用编排:断言已保存的行、已发出的事件、响应正文、磁盘上的文件,或简单日志伪造对象记录的效果。不要模拟值,而应自行构造。模拟由你负责的角色,不要直接模拟第三方库;应将提供商 SDK 封装在自有接口之后,并为真实提供商的数据形态添加契约/VCR 检查。对于交互测试,允许查询,预期命令:getter 可以自由调用;只有产生副作用的命令才值得设置预期。

对宽泛输入空间使用属性测试

当行为涉及的输入或操作空间比示例能够经济覆盖的范围更广时,使用基于属性的测试。选择能够说明有意义行为的属性,例如往返、一致性、守恒、蜕变关系、代数定律,或与小型模型的结果一致,而不只是验证执行时没有崩溃。

让生成器与待测行为相匹配:使用任意输入测试全域性和文档所述的错误;使用符合规范的输入测试语义分支;使用有状态轨迹测试生命周期行为;使用语料变异进行覆盖引导的发现。使用独立于实现的判定依据。属性测试工具负责构造值并缩减失败用例;覆盖引导的模糊测试器则会变异语料,以探索新的控制流。不要混用这些术语或工具。

对于较小的有限空间,在检查每个单元的成本以及是否有更低成本的层能够判定该属性(references/exhaustive-testing.md)后,优先采用穷举生成,而不是抽样。关于生成器、判定依据、执行和重放的指导,请参阅 references/property-based-testing.md。

测试错误处理路径,而不只是无效输入

大多数关键的生产故障都发生在浅层:它们位于错误处理代码中,只有在依赖项已经失败时才会运行,例如磁盘写入错误、连接中断、超时或部分响应。对灾难性分布式系统故障的实证研究发现,其中大多数都能通过触发这些路径的简单测试来复现。因此,“异常路径”不只是拒绝错误参数:还要注入下游故障(例如会抛出异常的 fake/stub、注入 I/O 错误或 side_effect 异常),并断言系统能够正确降级。

断言代码实际具有的失败行为,而不是你希望它具有的行为。阅读规范或观察代码,了解故障时应该采取什么措施——传播、重试、包装、回退还是回滚——然后据此测试。不要虚构重试次数限制、自定义错误类型或契约从未承诺的回滚语义;这会测试一份虚构的契约,属于范围蔓延。如果预期的故障行为未明确规定,就对代码当前的行为进行特征测试,并将这一缺口标记为后续事项,而不是在测试中设计新的生产行为。

将内部不变量纳入类型/模式/契约

类型和测试回答的是不同问题:

  • 类型/模式/契约:“这种无效状态是否可能存在?”
  • 测试:“可观察行为是否符合规范?”

如果某个不变量在多个内部层中重复出现,可以考虑将其提升到边界解析器、智能构造函数、模式、密封枚举、状态机或数据库约束中。在删除检查或相关测试之前,请确认以下各项:

  • 上下文属于内部环境,且不存在恶意输入,
  • 类型/模式/构造函数确实强制执行了该不变量,
  • 边界测试覆盖了恶意或格式错误的输入,
  • 安全/身份验证/外部故障层负责防御其他故障模式,
  • 生产类型变更属于范围之内,或已获明确批准。

如果用户要求“仅修改测试”或“不要更改生产代码”,就为当前 API 添加测试,并将收紧类型/模式列为后续事项。

在竞态检测器下测试并发代码,并明确并发契约

对于供多个 goroutine/线程使用的代码,顺序测试几乎无法证明什么。让多个工作线程同时启动并操作共享状态(通过启动屏障最大限度地增加竞争),并在竞态检测器或线程消毒器下运行(go test -race、ThreadSanitizer / -fsanitize=thread,以及通过 -count 重复运行)。将并发测试通过视为比顺序测试更弱的证据——竞态具有概率性。

断言 API 实际承诺的不变量——没有更新丢失、计数单调递增/线性一致、最多计算一次——而不是记录碰巧观察到的计数后就此作罢。如果实现可能违反该契约(例如在锁外计算,导致两个调用方重复计算),这就是应通过失败的测试或显式标记揭示的缺陷,而不是用 t.Logf 悄悄将竞态固化为预期行为。

模式工作流

编写模式
  1. 阅读相邻的测试/配置,并加载相关语言参考文档。
  2. 根据 references/test-types.md 选择测试层级。
  3. 对于 Bug/新行为,尽可能先添加失败的回归测试。
  4. 将用户/客户示例转化为易读的测试用例,而不必强迫每个示例都经过 UI。
  5. 当设置代码的杂音掩盖了测试意图时,使用构建器/工厂/辅助函数;当自定义辅助函数变成一个小型 DSL 时,为其编写测试。
  6. 优先使用面向用户的/公开接口,而非内部实现。
  7. 固定非确定性因素:时间、随机性、网络、文件系统、顺序。
  8. 有意识地选择测试值:包含不同于默认值的独特值,确保至少一个用例能够暴露被丢弃、被设为默认值或被交换的参数。同时,在零值、空值和相等值属于边界情况或契约内容时,也要覆盖这些情况。预期结果应独立于被测系统(SUT)得出:简单示例用字面量最清晰,较广泛的情况则适合使用属性或真正独立的参考模型。不要用 SUT 自身的逻辑或常量重新计算预期结果,否则测试可能与它共享同一个 Bug,却仍然通过。
  9. 先运行最相关的测试,再在可行时运行范围更广的检查。

对于转换或复杂的生成输出,使用黄金文件,并明确执行审查流程;参见 references/golden-file-testing.md。对于外部 API,优先使用录制的真实夹具/录制回放或契约检查,而非在 CI 中调用实时服务;参见 references/vcr-cassettes.md。

评估模式

按严重程度报告证据,并列出正面观察。对于生成式测试,在建议针对特定引擎进行更改之前,先加载相关语言和主题参考资料。检查:

  1. 是否在运行,以及它是否可能失败? 对于每个层级:运行它的任务、它在默认分支上的上次结果、受门禁控制的测试是否可达,以及每个门禁是否可能变红(|| true、continue-on-error、基线更新器、skip-green、空目标)。失效的门禁是首要发现。参见 references/gate-integrity.md。
  2. 破坏测试/虚假信心:跳过或聚焦的测试、没有断言、只记录日志而不做断言、已注释掉的断言、恒为真的断言。
  3. 判定依据的强度:唯一断言过弱、缺少负向/错误/状态/结构断言、同义反复。对于属性测试,如果存在语义不变量,那么仅验证不会崩溃的判定依据就很弱。
  4. 模拟与现实脱节:硬编码的模拟无法发现真实 API/模式变更。
  5. 层级完整性:单元测试访问真实网络;集成测试模拟了它们声称要测试的所有边界;端到端测试模拟被测系统或通过测试外观驱动系统;PR 套件面向生产环境;以及被测产物是否就是部署的产物。
  6. 确定性:休眠、墙上时钟时间、未设种子的随机性、顺序依赖、全局状态泄漏。
  7. 覆盖质量:分支覆盖率;对于覆盖率很高但仍有缺陷漏出的测试套件,在关键路径上植入故障,看看是否有测试会失败,并从成本最低的方式开始(references/gate-integrity.md §5)。不默认建议设置常设变异测试流程。
  8. 不变量放置位置:本应由类型/模式/契约承担,却被重复进行的内部验证。
  9. 生命周期匹配度:一次性探测被过度测试、可复用资产测试不足,或仅涉及结构的更改却进行行为测试表演。
  10. 示例质量:业务示例埋在 UI 脚本中、测试夹具难以阅读,或辅助工具组成了一种 DSL,却没有自己的测试。
  11. 生成式证据:所配置的运行器是否会触达生产路径、生成器是否覆盖声称的领域、判定依据是否独立,以及种子重放是否被误称为主动发现。若存在多个运行器、项目、筛选器或重复的目标列表,导致可达性不明确,请核实确切的 CI 配置;仅在风险反复出现时添加永久漂移保护。
  12. 测试代码可读性(DAMP):测试主体中的逻辑(循环/分支/计算得出的预期)、从 SUT 自身常量推导出的预期、在远离相关断言的位置被修改的共享测试夹具。参见 references/antipatterns.md。
  13. 测试套件结构:统计每个层级的测试数量;指出倒金字塔/冰淇淋筒(端到端测试占多数,单元测试基础薄弱)以及沙漏形结构(单元测试 + 端到端测试,中间的集成测试为空)。对于每个有更低层级归属的端到端测试用例,建议将其移至最低层级——业务规则应下沉到 UI 之下——同时保留少量覆盖黄金路径/关键旅程的端到端测试作为核心。
升级模式

按风险优先处理:

  1. P0:安全性、身份验证、注入和清理器测试中的断言较弱或缺失。
  2. P1:只断言非空、非空字符串或真值的测试。
  3. P2:不稳定的测试:休眠、实时网络、墙上时钟、顺序耦合。在修补等待逻辑之前,先询问能否在更小的 SUT 中运行测试——大小(依赖项、进程、RAM 占用)比工具选择更能预测不稳定性——并将自动重试/隔离视为掩盖债务,而非修复。
  4. P3:依赖大量模拟对象的“集成”测试,以及与实现耦合的模拟对象。
  5. P4:夹具噪声、重复内容、名称不清晰、缺少构建器。

常见升级方式:

  • 将 sleep() 替换为虚拟时间、注入时钟或基于条件的等待。
  • 将实时 API 调用替换为录制的磁带或已提交的夹具。
  • 将宽泛的模拟对象替换为伪造对象、契约测试或针对公开行为的断言。
  • 将重复的对象字面量替换为构建器,并明确保留与行为相关的字段。

绝不悄悄降低测试等级:按等级列出已删除或替换的测试;除非有同级或更高级别的测试替代,否则保留真实引擎测试(SQLite、workerd、浏览器)。只有检查能够强制执行的教训或事后复盘才算数。

检测模式

使用 references/antipatterns.md 中的具体搜索信号:

  • @skip、skip、skipif(没有真实条件时)、test.only、fit、xit、xdescribe。
  • 使用 print、console.log、t.Log、Debug.WriteLine 代替断言。
  • 无断言的测试,或只检查真值/非空字符串/非空值的测试。
  • 没有独立预言的属性测试、生成器被筛选为单一简单路径的测试,或对可穷举的小型有限域进行采样的测试。
  • 用于同步的 sleep、waitForTimeout、Thread.Sleep、Task.Delay。
  • 吞掉异常的 try/catch。
  • 与断言完全相同的模拟返回值。
  • 修改全局状态/环境变量/注册表却未清理。
  • 更新快照/黄金文件却不检查差异。
  • 使用 || true、continue-on-error、scan --baseline 或 ::warning:: 跳过步骤的 CI 步骤;从不运行的覆盖率或变异测试阈值。
  • 将变异测试作为程序运行:在代码未更改时按日历安排、使用整轮数字下限,或仅为消除残留变异而导出或固定内部实现。
  • 只在可能为空的结果循环中进行断言;没有非空检查的 violations == [];未从被测代码导入任何内容的测试。
  • 将源代码、CSS 或工作流文本读入并作为字符串断言;辅助层级的重试循环;调高全局超时,或使用 isCI ? a : b 设置时限。

验证循环

编写或修改测试后:

  1. 运行最近的相关测试命令。
  2. 如果计划采用修复 Bug 的 TDD,请分别报告红色阶段和绿色阶段的证据;如果未观察到修复前的失败运行,应将红色阶段标记为未验证,而不是声称已完成 TDD。
  3. 检查改动的测试中是否存在脆弱的单一断言、跳过/聚焦标记、只记录日志却不做断言、休眠、实时网络访问,以及与实现细节的耦合。还要检查:预期结果是否来自独立于 SUT 的判定依据,至少一个测试值是否能暴露被丢弃、设为默认值或互换的输入,是否仍覆盖相关边界值,以及断言是否仅针对行为涉及的字段。
  4. 对安全或转换测试,验证拒绝/移除行为和保留行为。
  5. 对不变量相关工作,在适用时验证两种策略:属性/不变量证明和无效状态的可达性。
  6. 对属性/模糊测试,确认所声称的输入域、独立判定依据、引擎原生重放,以及在配置不确定时,使用确切 CI 命令进行收集或目标选择。
  7. 根据退出状态判断通过或失败,而非根据输出文本。运行确切的 CI 入口,而不是子模式;使用其标签编译你修改过的、由标签控制的文件。
  8. 修复 CI 时,将“更新过时的选择器”与“弱化断言或延长超时”分开处理,并说明每项弱化的理由。只有在干净检出基线提交后复现失败,才能称其为已有失败。
  9. 如果验证受阻,请报告确切命令、失败情况和次优检查方案。切勿声称测试已通过,除非确实运行过;说明绿色结果来自哪个环境,并在存在已推送提交时检查其 CI。

最终报告要求

测试工作结束时,使用:

Tests changed/assessed:
Behavior covered:
Commands run:
Results:
CI status (ran / red / not running):
TDD evidence (if claimed):
Gaps / risks:
Follow-ups:

保持简洁,同时确保验证情况和剩余风险可供审查。

评论

支持 Markdown