mcpField
A
awesome-test-writing

awesome-test-writing

github.com/khasky测试与质量2283 次安装1 周前更新
代码仓库

关于 awesome-test-writing

awesome-test-writing 是面向开发者的测试编写技能,强调沿用项目现有约定,并验证测试能捕获真实回归。

主要功能

  1. 01沿用现有测试约定
  2. 02按缺陷风险选择测试层级
  3. 03预先确认公开测试接口
  4. 04编码前设计行为用例
  5. 05验证测试先失败再通过
  6. 06针对性运行变异测试

适用场景

  1. 01为 Bug 修复添加回归测试
  2. 02为遗留代码重构建立安全网
  3. 03用模糊或属性测试检查输入解析

安装

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

$npx skills add https://github.com/khasky/awesome-agent-skills --skill awesome-test-writing

安装后在 Claude Code 中输入/awesome-test-writing确认已加载。

附带文件evals/eval_queries.jsonreferences/fuzzing.md

详细介绍

编写测试

编写行为出问题时会失败、重构时仍能通过的测试。交付成果不是覆盖率,而是一道触发线:对于被测代码所有合理的回归方式,至少有一个测试会变红。无法在代码出错时失败的测试,不过是穿着测试伪装的文档。

何时启用

  • “为 X 编写测试”“增加覆盖率”“这个模块没有测试”。
  • 修复 bug 需要回归测试(awesome-bug-fix 在此移交)。
  • 评审或审计发现指出“需要测试”(awesome-code-review、awesome-security-audit、awesome-dependency-audit)。
  • 重构前需要为遗留代码提供安全网(特征测试)。

不要在评审差异中的测试质量时启用(由 awesome-code-review Phase 4 负责),也不要在查找故障根因时启用(由 awesome-bug-fix 负责)。

工作流程

  1. 先了解现有的测试配置:运行器、断言风格、fixture/工厂约定、文件放置位置和命名模式。新测试应遵循仓库现有的形式;切勿引入第二套测试框架或并行约定。如果完全没有运行器,请提出该生态系统的默认选择并等待批准(这会引入新依赖)。
  2. 从头到尾阅读待测代码:公共接口、输入、输出、副作用和错误路径。测试所用的接口必须是调用方实际使用的接口;如果必须导出私有函数才能测试,应将其作为设计信号报告,而不是绕过问题。
  3. 选择能捕获缺陷的最低测试层级:纯逻辑使用单元测试;接线、查询和契约使用集成测试;只有关键用户流程才使用 E2E。一个单元测试就能捕获的 bug,如果只能由 E2E 测试套件捕获,那么每次运行的成本都会永久增加 100 倍。
  4. 写一行测试代码之前,先就测试接口达成一致:写下测试将针对哪些接口(公共入口、要通过哪些边界进行观察),并把清单交给用户。未经确认的接口一律不测。无法测试所有内容;提前就接口达成一致,才能把精力放在关键路径和复杂逻辑上,而不是平均分散到每个边缘情况;这也能在分歧仍易于处理时及时暴露,避免在错误层级写完 20 个测试后才发现。若接口的形态本身就是待讨论的问题(模块应该有多深、接口应该放在哪里),awesome-architecture-audit/references/design-vocabulary.md 中列出了相关术语;即使未安装该技能,接口清单仍然具有约束力,只是缺少双方共享的术语。
  5. 写代码之前先设计测试用例清单:正常路径、边界情况(空、一个、多个、最大值)、错误路径(无效输入、依赖失败、超时),以及这段代码容易出现的 bug 类型(分页的差一错误、日期计算中的时区问题、检查后执行引发的竞态)。某个保护、捕获、回退或规范化逻辑之所以存在,是因为某种输入曾经需要它:找出该输入并将其列为用例。阅读代码可以找到用例,但找不到用例的预期结果:每个预期值都必须来自契约(文档、类型、调用方需求),因为照抄代码当前返回的值会让它的 bug 变成测试预期。针对每个状态或枚举值,用两份清单逐一核对每个使用方:定义中声明的值,以及实际处理这些值的分支,并标注行号;第一份清单中有、第二份清单中没有的值,就是尚未测试(且通常未处理)的用例。写下清单;每个用例对应一个测试,且只关注一种行为。
  6. 以行为为先:通过公共接口断言可观察到的结果(返回值、状态变化、发出的事件、记录的调用),绝不检查内部细节(私有字段、辅助函数的调用顺序)。测试名称应说明场景和预期结果(“使用过期令牌时返回 401”,而不是“test token 2”)。
  7. 证明每个测试都能失败:针对故意破坏的代码运行新测试(撤销修复、反转分支、破坏常量),确认测试变红;然后恢复代码并确认测试变绿。回归测试必须双向验证:没有修复时失败,有修复时通过。一个从未失败过的测试,什么也证明不了。
  8. 运行测试套件并报告结果:覆盖所有相关范围,读取退出代码,并报告不稳定行为(只在重跑后通过的测试是缺陷,不算通过——应隔离并报告,绝不悄悄重试直到变绿)。分别报告新增测试的通过、失败、出错和跳过数量:设置错误、缺少 fixture,或因缺少依赖而跳过,都不算通过;被跳过的新测试毫无证明力。

分领域规则

  • 测试夹具和工厂 — 每个测试都拥有自己的数据:工厂支持按测试覆盖,不共享可变种子数据集,也不让测试依赖其他测试留下的数据。集成测试要按测试隔离(事务回滚、每个测试使用独立 schema,或使用唯一键)。
  • 确定性 — 冻结时间,并在 TZ=UTC 下运行;为随机性设定种子或注入随机数;绝不使用 sleep——应等待条件满足,并设置截止时间。依赖墙上时钟、网络状况或执行顺序的测试,注定会不稳定。
  • 模拟 — 只模拟进程不拥有的对象(外部 HTTP、第三方 SDK、支付服务提供商);边界内的所有内容都使用真实代码。进行模拟时,要断言发给模拟对象的请求(载荷、标头、幂等键),而不只是检查预设响应的影响。
  • 特征测试(旧代码)——重构未经测试的代码前,先通过测试固定其当前可观察行为,断言它实际做了什么(包括奇怪之处),而不是它应该做什么;这些测试是让重构可验证的安全网;一旦行为有意改变,就要重新审视这些测试。
  • 属性测试和模糊测试 — 任何用于解析不可信字节的代码(文件格式、协议帧、用户标记语言)都应覆盖基于属性的测试或模糊测试,并将失败样本集提交为回归种子;仅靠基于示例的测试,必然会漏掉攻击者尝试的输入。一组固定的恶意输入测试夹具(../../etc/passwd、' OR 1=1--、<img onerror=…>、10MB 字符串、RTL/零宽 Unicode)应针对每个处理输入的边界运行。任何序列化/解析或编码/解码组合都要具备往返属性:对生成的输入验证 decode(encode(x)) == x,其中包括空值、最大值和非 ASCII 值——无需手写任何示例,就能捕获一侧遗漏的字段。关于测试框架规则(确定性、大小验证、重置全局状态)、各生态系统的模糊测试工具(libFuzzer/AFL++/cargo-fuzz/Atheris/Go)、消毒器构建(ASan/UBSan)、将覆盖率用作信号,以及 wycheproof 加密测试向量,请参阅 references/fuzzing.md。当被测边界要解析不可信字节时,请阅读该文档;普通单元测试则不需要。
  • 红 → 绿循环 — 每个循环只处理一个边界、一个测试和一个最小实现,以垂直切片的方式推进:编写失败的测试,观察它失败,只编写足以让它通过的代码,然后重复。先写完所有测试,再写完所有实现,测试的将是想象中的行为——测试固定的是你猜测的形态,而非最终发现的重要行为,也就无法敏锐地捕捉关键变化。重构不是这个循环的第三步:调整代码结构属于审查阶段(awesome-code-review,或适用于整个仓库的 awesome-code-cleanup),而这里编写的测试正是确保重构安全的保障。
  • 覆盖率 — 是一个较弱的信号,绝不是目标:用它查找你原本打算测试、却尚未覆盖的分支,不要为了追逐数字而测试。带镜像断言的 100% 覆盖率,保护效果为 0%。
  • 变异测试 — 测试套件层面的第 7 步:第 7 步验证某个测试能失败;变异测试则检查整个测试套件能否发现代码发生了变化。工具会改写生产代码(翻转比较、交换边界、删除语句、返回常量),并报告有多少变异未被测试杀死。存活的变异体指出了一个明确的漏洞:测试套件执行了某行代码,却没有检查它。按范围运行——选一个模块,比如处理金额、身份验证或解析器的模块——首次运行时绝不要覆盖整个仓库;完整的仓库级变异测试要消耗数分钟到数小时的 CPU,最终生成的报告却无人阅读。各生态系统的工具:Stryker(JS/TS、.NET、Scala)、mutmut/cosmic-ray(Python)、PIT(JVM)、cargo-mutants(Rust)、go-mutesting(Go)。针对存活的变异体采取行动,而非盯着分数——为了追逐变异百分比而追逐,本质上和为了覆盖率数字而凑数无异。等价变异体(无法改变可观察行为的改动)只是噪声:标记后继续。
  • 其他语言 — 以上内容与测试运行器无关;只有实现方式的名称会变(fake timers、freezegun 或注入时钟;t.Parallel() 隔离或事务回滚)。从仓库现有的测试中了解惯用做法,而不是照搬其他生态系统的习惯。

输出

Tests written — <module / behavior> — <N> tests

Cases covered:
- <scenario> → <expectation>   [unit|integration|e2e]
...

Proven to fail: <which mutation/revert made each go red>
Run: <command> → <N passed, 0 failed, 0 errored, 0 skipped, exit 0>  (new tests counted apart from the existing suite)
Not covered (and why): <cases deferred, with reason — not silence>

反模式

反模式应改为
镜像断言——预期值的计算方式与代码相同根据规范或手动计算,硬编码已知正确的值
变更检测测试——对内部实现做快照,任何重新设计都会触发测试断言可观察到的契约;仅对真正的输出界面做快照
断言轮盘——有许多裸断言,却不清楚哪个失败了每个测试聚焦一种行为;对不明显的断言添加说明
测试模拟对象——断言存根返回了传给它的值断言系统围绕模拟对象的行为,以及传入模拟对象的请求
只检查形状的断言——只检查值是否存在,不检查具体内容断言值本身
只检查第一个匹配项——在多个项中找到一个正确项就停止断言整个集合:数量、顺序,以及不包含任何错误项
单边负向测试——拒绝时,只断言某一种后果没有发生断言所有后果都没有发生:没有写入、没有事件、没有副作用,状态未改变
验证器会优先拒绝的变异或边界输入使用能够通过验证的输入,这样测试的就是逻辑而非验证器(单独测试验证器)
在现有运行器旁引入新的框架使用仓库的运行器,即使其他运行器更好用
为覆盖率凑数——测试琐碎的 getter,却让错误路径缺少测试根据行为和风险列出测试用例,优先测试边界和失败情形
修复后编写且从未见过失败的测试撤销修复,观察测试变红;恢复修复,观察测试变绿——每次都要双向验证

评论

支持 Markdown