mcpField
T
test-suite-curation

test-suite-curation

github.com/petrkindlmann测试与质量2221.1k 次安装6 天前更新
代码仓库

关于 test-suite-curation

test-suite-curation 是面向测试与 QA 团队的回归测试集审计技能,依据覆盖指纹、变异测试、代码相似度和 CI 历史识别候选冗余项。它将删除视为需隔离观察、人工批准并留存记录的破坏性变更。

主要功能

  1. 01构建逐测试覆盖指纹
  2. 02用变异测试验证冗余候选
  3. 03聚类相似测试以供审查
  4. 04挖掘 CI 历史中的失败与波动
  5. 05按价值分类并分层测试集
  6. 06删除前隔离、审批并记录

适用场景

  1. 01找出仅由单个测试覆盖的分支
  2. 02验证覆盖相同的测试是否可替代
  3. 03调查从未失败或经常波动的测试
  4. 04安全清理并整理回归测试集

安装

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

$npx skills add https://github.com/petrkindlmann/qa-skills --skill test-suite-curation

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

包含的文件

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

references/
audit-record.md
ci-history-mining.md
clustering.md
coverage-fingerprinting.md
destructive-safety.md
mutation-confirmation.md
tiering.md

详细介绍

测试 A 和测试 B 覆盖的行完全相同,因此疲惫的工程师删掉了 B——三周后,生产缺陷却漏了过去,因为只有 B 验证了舍入结果是否正确。覆盖率相同不代表冗余。此技能将整个回归测试套件作为一个整体进行审查(进行人工无法手动完成的冗余分析),依据证据而非直觉来精简测试,并将每次删除都视为破坏性变更:必须经过隔离宽限期、人工签核,并留下可向审计人员说明的记录。

快速导航

你想要……前往
找出每个测试独有覆盖的行/分支覆盖指纹
判断“覆盖相同”是否意味着“删掉一个”覆盖率相同的陷阱
找出复制粘贴的近似重复测试近似重复项聚类
找出从未失败和总是偶发失败的测试挖掘 CI 历史记录
判断测试属于冗余、过时、价值较低还是应该保留精简决策规则
将扁平的测试套件拆分为 smoke/core/extended分层
安全删除已标记的测试破坏性操作安全措施
生成“删除了什么以及原因”的记录审计记录

发现问题

首先,检查项目根目录中的 .agents/qa-project-context.md,跳过其中已解答的问题。然后明确以下事项:

  • 使用哪种语言/运行器? pytest+coverage.py、Jest/Vitest+Istanbul、Go、JUnit——每种技术栈的单测试上下文机制都不同(变异测试工具也不同)。
  • 是否有 CI 测试结果历史记录,能追溯多久? 没有 JUnit/Datadog/Trunk 历史记录,就无法挖掘从未失败或偶发失败的信号——只能依靠覆盖率和聚类分析。
  • 测试套件规模和当前耗时是多少? 这决定分层目标(例如,smoke 在 5 分钟内完成),以及单测试覆盖率能否在一次运行中完成,还是必须分片运行。
  • 业务风险图/关键路径是什么? 分层和“保留”判定都取决于此。如果缺少,请先运行 risk-based-testing。
  • 由谁签核删除操作,是否有 CODEOWNERS 文件? 删除属于破坏性操作;此技能移除任何内容前,必须指定审批人。
  • 可接受的观察窗口是多久? 团队愿意将隔离的测试标记为跳过并继续运行多长时间,之后才永久移除(默认:2 个冲刺/2 个发布周期)。

核心原则

  1. 覆盖率相同不等于冗余。 两个测试执行了相同的代码行,也可能断言完全不同的内容——使用不同的判定依据、输入和边界情况。行覆盖率/分支覆盖率只能说明代码运行过,无法说明代码经过了哪些检查。证明一个测试被另一个测试取代的唯一依据,是保留下来的测试能够捕获相同的故障;这需要通过变异测试来证明,而不是比较覆盖率差异。

  2. Agent 的优势在于分析整个测试集,而非决定删除什么。 Agent 可以在几分钟内为 4,000 个测试生成特征、将近似重复项分组,并交叉对照 CI 历史记录——这是人类无法手动完成的工作。这就是它的全部价值。但 Agent 负责提出建议,由人类批准。绝不要让面向整个测试集的分析演变成自动删除整个测试集。

  3. 删除具有破坏性,必须确保实际可恢复。 “Git 历史里有”不算恢复方案。先隔离(跳过/标记为 xfail,或移至已弃用的测试套件),在规定的观察期内监控是否有缺陷漏网,然后经签字批准再删除。宽限期才是安全保障,而不是提交日志。

  4. 每种处置方式都应明确区分。 冗余、过时和价值低是三种不同状态,应采取三种不同措施。把它们都归入“删除”这一类,就会导致真正的覆盖率丢失。从未失败过的测试,不等于不会失败的测试。

  5. 以证据而非直觉为准,并为每个测试记录证据。 每次删除都要有对应记录:类别、取代它的测试、覆盖率变化、批准人以及恢复方法。如果无法填写这条记录,就不能删除该测试。


1. 覆盖率特征分析(按测试)

错误的做法是使用单份合并的 --cov 报告,只查看每个文件的百分比。这无法告诉你哪些测试覆盖了哪些代码行,因此也无法判断哪些测试存在重叠。你需要使用按测试划分的(动态)上下文。

pytest/coverage.py——通过动态上下文记录每个测试命中了哪些代码行,并启用分支覆盖率:

pytest --cov=src --cov-branch --cov-context=test

--cov-context=test 会让 pytest-cov 为每个 pytest 测试 ID 和执行阶段(setup、run、teardown)切换 coverage.py 上下文。参数化测试的 ID 仍各不相同。比较特征前,先合并每个测试的三个阶段;同时保留各阶段的原始数据,以检查夹具的归属。dynamic_context = test_function 会记录函数上下文,但无法提供相同的 pytest 阶段/参数身份信息。添加 --cov-branch 可区分已执行的控制流路径。结果会写入 .coverage。

然后从 .coverage SQLite 数据库中读取上下文表,构建按测试划分的特征:对每个测试,准确记录其覆盖的 (file, line) 和 (file, branch-arc) 对集合。利用这些集合可以计算:

  • 唯一覆盖的代码行——只有一个测试覆盖的代码行/分支。失去该测试,就会彻底失去这部分覆盖率。这些测试发挥着重要作用,应予以保护。
  • 覆盖关系——测试 A 的覆盖集合是测试 B 的超集。这表示它可能冗余(但请参见 §2——这并不能作为证明)。

请参见 references/coverage-fingerprinting.md,了解如何使用 SQL 从 .coverage 中提取上下文、用于构建按测试划分的代码行/分支集合并计算唯一覆盖项及覆盖关系的 Python 代码,以及 JS/Vitest + Istanbul 的 --coverage 对应方案(使用按测试划分的报告器生成 coverage-final.json)。

不要按每个测试文件的代码行数对测试进行排序,也不要仅因测试的总体覆盖率百分比较低就删除测试——只有一行的测试也可能是保护关键分支的唯一防线。


2. 覆盖率相等陷阱(关键规则)

这是本技能中最重要的一条规则。按测试统计的数据表明,测试 A 和测试 B 覆盖的行完全相同时,天真的结论会是“重复了,删掉一个”。这是错误的,关键点如下:

覆盖率相等并不能证明测试具有相同的断言、相同的输入或相同的判定标准。 两个测试可能覆盖完全相同的行,但一个断言 HTTP 状态,另一个断言响应正文;或者它们传入了不同的边缘情况输入。一个测试覆盖相同行,但断言不同的值;另一个覆盖相同行,但检查不同的状态。覆盖率衡量的是执行情况,而非验证情况。

要判断 B 是否确实冗余——也就是 A 是否真正涵盖了 B 的故障检测能力——请运行变异测试:

  • Python 使用 mutmut(3.6.0+)或 cosmic-ray,JS/TS 使用 StrykerJS(9.x),Java/JVM 使用 PIT,Rust 使用 cargo-mutants。
  • 变异测试会在被覆盖的代码中注入故障(变异体)。如果测试在变异后的代码上失败,就称该测试“杀死”了变异体。如果 A 杀死了 B 杀死的所有变异体,那么 A 确实涵盖了 B 的故障检测能力,删除 B 就有充分依据。如果 B 杀死了一个 A 未能杀死的变异体,那么 B 能捕获 A 无法捕获的一类缺陷——保留 B,即使覆盖率相同。

决策: 覆盖率相同 → 标记为候选项 → 通过变异测试确认 → 之后才能建议删除。当断言不同且变异测试结果也不同时,保留两者,不要删除。请参阅 references/mutation-confirmation.md,了解如何配置 mutmut/Stryker,将变异测试限定在可疑行范围内,以及如何比较杀死集合。


3. 近似重复测试聚类

目标:找出复制粘贴的测试,而不是把同一文件中的每个测试都标记出来。按文件名对测试分组不等于聚类——它无法说明相似度。可结合以下两个可靠信号:

  1. AST(抽象语法树)相似度。 将每个测试解析为 AST,对标识符名称和字面量进行归一化处理,然后比较结构。使用 token/tree 相似度指标(对归一化 token shingles 使用 Jaccard,相对于 AST n-gram 使用余弦相似度,或使用树编辑距离)。基于 AST 的比较不会受到格式和变量名差异的干扰,而这些差异会影响精确字符串匹配或原始 diff。切勿使用原始行号作为相似度信号。

  2. 覆盖率配置签名。 根据 §1,每个测试都有其覆盖行/分支集合,即其执行配置。覆盖率签名近乎相同,且 AST 近乎相同的测试,是很有可能近似重复的候选项;单独使用任何一个信号都不够可靠。

使用可调的相似度阈值进行聚类(例如,采用凝聚式/层次聚类,并在可配置的截断值处切分——从约 0.85 开始,再根据你对误报的容忍度调整)。输出聚类结果,绝不直接删除测试。

每个聚类都必须交由人工审核。 Agent 不会自动删除整个聚类——复制粘贴的测试经常会在某个关键断言上有所不同。展示每个聚类的成员、两两相似度以及覆盖率配置重叠度,由人工确认哪些(如果有)可以合并。

请参阅 references/clustering.md,了解 AST 归一化、shingle/Jaccard 和树编辑相似度函数,以及采用可调阈值的凝聚式聚类。


4. 挖掘 CI 历史记录

解析你的测试结果历史记录——JUnit XML 存档,或已存储这些记录的平台:Datadog Test Optimization、Trunk Flaky Tests、BuildPulse、CircleCI test insights。为每个测试计算通过率/失败率,以及翻转率(连续运行从通过→失败或从失败→通过的频率)。两项发现,含义截然不同:

从未失败的测试(在统计窗口内零失败/通过率为 100%)。最简单的做法是删除所有一次都没失败过的测试。错——从未失败不代表应该删除或毫无用处。测试之所以常常从未失败,是因为它保护的是变更少、风险低且稳定的代码——恰恰是无人改动的代码,因此测试从未触发。这只意味着它在当前统计窗口内的缺陷检测信号较弱,并不代表它毫无价值。处置方式:调查,不要删除——检查被测代码的变更频率和风险。如果它覆盖的是一条尚未发生回归的关键路径,就应保留。

总是出现波动的测试。 是否存在波动不能根据单次运行来判断。真正的定义是同一 SHA/同一提交产生不同结果——同一份代码既通过了测试,也未通过。按提交 SHA 对运行结果分组,并找出在同一 SHA 上产生过两种结果的测试,即可检测这类情况(Trunk 和 BuildPulse 原生支持此功能)。最简单的做法是“删除有波动的测试,清理 CI”。错:有波动的测试可能仍是你对某条真实路径的唯一覆盖。处置方式:隔离并修复波动问题,绝不要为了清理 CI 而删除测试。隔离可立即减少 CI 噪声;同时仍要修复根本原因。有关单个有波动测试的运行时隔离和自愈,请参阅 test-reliability。

请参阅 references/ci-history-mining.md,了解 JUnit-XML 聚合脚本、同一 SHA 上的波动测试查询,以及 Datadog/Trunk API 数据拉取。


5. 剪枝决策规则

不要再删除所有“看起来冗余”的测试。三种失败类别是不同的状态,各自对应不同的处置方式:

类别定义(该测试……)处置方式
冗余被另一个测试涵盖——覆盖相同的代码行,且保留的测试能杀死相同的变异体(§2)合并或删除——但必须先通过变异杀死检查确认其确实被涵盖;先隔离
过时测试的功能已被移除/代码已无效/路径已不存在删除——被测代码已不存在;确认目标确实已不存在,再移除测试
价值低从未失败,且内容过于简单(getter、空操作、没有有意义的断言)隔离/转交审核——价值低不等于毫无价值;移除前先确认
保留独特地覆盖某些内容、能捕获缺陷(有正向的缺陷检测历史),或保护高风险路径保留——无论覆盖是否重叠,都应保留

原则:每种类别采取不同的操作。 redundant => merge/delete-after-mutation-check、obsolete => delete、low-value => quarantine/review、keep => keep。对每个标记出的测试套用同一条一刀切规则,是导致覆盖丢失的反模式。请注意,“冗余”和“价值低”都应先隔离,而不是直接执行 rm。


6. 分层(smoke / core / extended)

将扁平的测试套件重组为多个层级,并不是按速度排序。Smoke 并非文件顺序中的前 N 个测试,也不是随机抽样,而且不得仅按每个测试的运行速度分层。分层应依据两项证据:

  1. 业务风险 / 关键路径 — 来自风险图(risk-based-testing)。保护收入、身份验证、数据完整性和主要用户旅程的测试,无论速度如何,都应归入 smoke/core。
  2. 缺陷检测历史 — 实际上发现过真实 Bug 的测试(挖掘 CI 历史记录,并关联 Bug 工单,找出在修复缺陷的提交上失败的测试)。有发现回归问题记录的测试,理应凭实力获得较高层级。
层级目标选择依据
Smoke快速运行,每次推送都运行,耗时几分钟,仅覆盖关键路径风险最高的路径 + 经验证能发现缺陷的测试;速度足够快,可用于门禁检查每次提交
Core每个 PR / 合并门禁覆盖所有关键和高风险场景,范围比 smoke 更广
Extended全量 / 每晚 / 耗时较长 / 发布前运行其他所有测试 — 全面、运行时间长、覆盖边界情况

允许将执行时间作为分层依据,但只能用作次要的平局判定因素,不能作为主要标准。 运行时长用于打破平局,不能决定层级。在风险相同的测试中,smoke 优先选更快的测试。将层级编码为标记/标签 — @pytest.mark.smoke / @pytest.mark.extended、Jest/Vitest 标签或 JUnit 类别 — 这样 pytest -m smoke 就能选出某个层级,无需移动文件。标记方案和缺陷检测历史查询见 references/tiering.md。


7. 删除安全(观察期)

如果被要求删除 600 个测试,天真的 Agent 会开一个 PR,一次性 rm 掉 600 个文件。绝不要在单个 PR 中删除整个测试群组,也不要 rm 测试文件。 删除具有破坏性;应设置门槛:

  1. 先隔离,再删除 — 绝不要直接删除。 将标记出的测试设为跳过(@pytest.mark.skip(reason="curation-2026-Q2, see audit row")、xfail、Jest .skip),或将它们移至仍保留在仓库中的 quarantine//deprecated/ 测试套件。这样它们不会运行,但仍可见,也能立即恢复。
  2. 必须获得人工批准。 指定的审批人必须批准,可通过测试目录中的 CODEOWNERS 指定,也可在 PR 中明确指定审阅者。不要以“冗余测试无需审查”为由跳过。直到 §2 确认之前,冗余都只是一个假设。
  3. 设定观察期。 在宽限期内运行隔离该测试群组的套件 — 默认 2 个冲刺 / 2 个发布周期 — 并监控遗漏的缺陷:留意生产事故和 Bug 工单,查看是否出现本应由被隔离测试发现的问题。观察期内若出现遗漏的缺陷,说明测试并非冗余;应恢复测试。
  4. 只有在此之后,才能分小批次删除,并为每批删除关联对应的审计记录(§8)。
  5. 确保可以恢复。 记录隔离位置和提交,以便一步恢复/还原任何测试。Git 历史只是最低保障,并非恢复方案。

隔离标记、测试路径的 CODEOWNERS 片段,以及遗漏缺陷监控检查清单见 references/destructive-safety.md。


8. 审计记录

让每项删除都能向未来的工程师或审计人员证明合理性的交付物,不是删除测试的数量,而是每项测试的记录——每个已删除的测试占一行——并且每项都要有实际的理由。只写一句“我们删除了 600 个冗余测试”是无法审计的。

每一行应包含:

  • 测试 ID / 路径 — 删除了什么。
  • 类别与理由 — 冗余 / 已过时 / 价值低,并附上具体理由(“已被涵盖;保留的测试会杀死相同的变异体集合”)。
  • 被取代项 — 涵盖或取代该测试的保留测试(取代它的测试)的 ID(针对冗余项):涵盖该测试的测试已将其包含在内,或取代它的测试已将其替换。
  • 覆盖率变化 — 删除后不再覆盖的行 / 分支(删除前 / 后);最好净损失为零,并由 §1 证明。
  • 审批 — 批准者姓名、PR/提交 SHA 和签字批准的日期。
  • 恢复路径 — 隔离位置,以及用于恢复 / 还原的单行命令。

将其存为已提交的 CSV/Markdown 表格(例如 docs/test-curation-log.md),以便与删除记录一同进行版本管理。完整的列定义和示例行请参见 references/audit-record.md。


反模式

1. “覆盖率相同,删掉一个”

这是破坏性最大的错误。行覆盖率相同,只能证明这些行运行过,不能证明断言相符。在提出任何删除之前,先通过变异测试 (§2) 确认其涵盖关系。

2. 把两个绿色信号当成放行信号

一项测试 18 个月从未失败,且被另一项测试完全覆盖(相同的行),这是经典的“肯定可以安全删除”案例。单凭这些还不够——仍需验证,不要自动删除。一项测试从未失败,可能意味着代码稳定,而不是测试毫无价值——这往往意味着风险低、变更少,而非测试冗余。不同的判定标准仍可能是这项测试独有的(不同的断言、输入或只有它会检查的边界情况)。要问变异测试的问题:另一项测试是否断言 / 捕获 / 杀死了这项测试所做的事情?只有完成这一步后,才可将其隔离并观察一段宽限期,然后再删除。这两个信号都是必要但不充分的条件(见下文 §9 / §2 / §7)。

3. 只进行一次合并覆盖率运行,不保留逐项测试的上下文

--cov=src 在没有上下文的情况下只能提供逐文件的百分比,无法识别逐项测试的重叠情况。请使用 --cov-context=test,并从 .coverage 数据库中读取逐项测试的上下文。

4. 按文件名将相似测试分组

文件名接近不代表相似。应基于 AST 相似度 + 覆盖率特征签名,使用可调阈值进行聚类(§3),并将各组交由人工处理——绝不要自动删除整个组。

5. 删除从未失败或不稳定的测试

从未失败 → 进行调查(代码可能很少变更),不要删除。不稳定 → 隔离并修复,不要为了清理 CI 而删除。多次运行结果不稳定指的是同一 SHA 下结果不一致,而非单次运行失败。

6. 对所有标记的测试采用同一种处理方式

冗余、已过时和价值低的测试需要采取不同的处理方式(§5)。把它们一律归入“删除”类别会损失实际覆盖。

7. 只按速度(或“前 N 个”)划分层级

Smoke 测试应基于风险 + 缺陷检测历史来划分,而不是选择最快的测试或前 N 个测试。运行时间是平局时的决胜因素,不是划分依据(§6)。

8. 一次性大规模删除 PR

在一个 PR 中通过 rm 删除 600 个测试,没有隔离、没有签字批准,也没有观察期。始终按照以下顺序操作:隔离 → 签字批准 → 观察 → 分批删除(§7)。

9. “Git 历史里有,直接删除就行”

Git 历史不是恢复计划——没有人会监控被删除测试本可以捕获的缺陷。隔离宽限期加上已逃逸缺陷监控,才是真正的安全网。


验证

在删除任何内容之前,先证明审计确实成立,从最小的检查开始:

  • 已捕获每项测试的上下文。 检查 .coverage 上下文名称,查找以 |setup、|run 或 |teardown 结尾的 pytest 测试 ID,并保留参数 ID。将汇总后的 ID 与已收集的测试进行比较,并调查缺失的情况;没有执行任何应用代码的测试可能没有上下文。原始上下文数量不等于测试数量,因为各阶段是分开的。
  • 没有任何删除仅凭覆盖率相同就成立。 对每个 redundant 行,审计日志的 mutation_check 列都已填写(yes + 运行 ID)。grep -c 'redundant' docs/test-curation-log.md 等于 mutation_check 非空的行数。
  • 先隔离,而非移除。 git log --diff-filter=D --name-only -- tests/ | grep test_ 显示隔离 PR 中没有删除任何测试文件;被标记的测试已跳过、标记为 xfail,或移至 tests/quarantine/,且仍可被收集(pytest --collect-only -m skip 或等效命令会列出这些测试)。
  • 分层选择正确。 pytest -m smoke --collect-only 仅返回风险/缺陷捕获测试组,并在 smoke 预算内运行;pytest -m "smoke or core or extended" --collect-only 涵盖所有测试(没有测试未打标签)。
  • 审计记录完整。 没有任何 redundant 行的 superseded_by、coverage_delta 或 mutation_check 为空;对于所有声称净损失为零的删除,coverage_delta 都是 0 lines / 0 branches。

完成标准

  • 为测试套件生成了逐测试覆盖率指纹(使用 --cov-context=test/--cov-branch,或 Istanbul 的逐测试等效功能),并计算了每项测试独有覆盖的代码行。
  • 每个“冗余”候选项都附有变异测试结果,证明保留的测试能杀死相同的变异体;不得仅凭覆盖率相同就提出删除。
  • 根据 AST + 覆盖率特征相似度,按明确阈值生成了近似重复测试簇,并提交人工审核;没有自动删除任何测试簇。
  • 从 CI 历史中挖掘出从未失败的测试(处置:调查)和同一 SHA 下的不稳定测试(处置:隔离/修复);不得根据这些信号删除这两类测试。
  • 每项被标记的测试都恰好归入 {redundant, obsolete, low-value, keep} 之一,并已执行相应处置。
  • 根据风险 + 缺陷检测历史分配了分层(smoke/core/extended)标记/标签;pytest -m smoke(或等效命令)可选择一个层级。
  • 未隔离一段时间、未记录指定审批人 / CODEOWNERS 的签字确认,或观察期内出现未被发现的缺陷时,不得删除任何测试。
  • 已提交逐测试审计记录;每项删除对应一行,包含类别、替代项、覆盖率变化、审批人 + SHA + 日期,以及恢复路径。

相关技能

  • ai-qa-review — 判断单项测试是否编写良好(异味、断言薄弱、可测试性)。本技能决定测试是否应该存在;ai-qa-review 决定现有测试是否优秀。整理后,对保留的测试运行 ai-qa-review。
  • coverage-analysis — 负责项目级覆盖率阈值、缺口分析和变异测试设置。本技能在冗余判定中使用逐测试覆盖率;如需设置递增门槛和 CI 门禁,请参阅该技能。
  • test-reliability — 在单项不稳定测试失败时,对其进行运行时自愈和隔离。本技能从 CI 历史中找出不稳定测试组;test-reliability 则逐项修复。
  • risk-based-testing — 生成本技能的“保留”处置和分层所依赖的风险矩阵。如果尚无风险图谱,请先运行该技能。
  • qa-project-context — 通用依赖项:驱动此处所有决策的技术栈、运行器、风险图谱和所有权信息。

参考文件(位于 references/)

  • coverage-fingerprinting.md — .coverage SQLite 上下文查询、按测试构建行/分支集合、唯一覆盖与包含关系计算,以及等效的 JS/Istanbul 按测试计算方法。
  • mutation-confirmation.md — 将 mutmut/cosmic-ray/StrykerJS 配置限定到可疑行,以及用于确认包含关系的杀死集比较。
  • clustering.md — AST 规范化、Jaccard/余弦/树编辑相似度,以及带可调阈值的凝聚聚类。
  • ci-history-mining.md — JUnit-XML 聚合、检测相同 SHA 易波动测试的查询,以及 Datadog/Trunk API 拉取。
  • tiering.md — 用于 smoke/core/extended 的标记/标签方案,以及缺陷检测历史查询。
  • destructive-safety.md — 隔离标记、测试路径的 CODEOWNERS,以及逃逸缺陷监控清单。
  • audit-record.md — 删除日志的完整列架构,以及一条填写示例。

评论

支持 Markdown