关于 test-suite-curation
test-suite-curation 是面向测试与 QA 团队的回归测试集审计技能,依据覆盖指纹、变异测试、代码相似度和 CI 历史识别候选冗余项。它将删除视为需隔离观察、人工批准并留存记录的破坏性变更。
主要功能
- 01构建逐测试覆盖指纹
- 02用变异测试验证冗余候选
- 03聚类相似测试以供审查
- 04挖掘 CI 历史中的失败与波动
- 05按价值分类并分层测试集
- 06删除前隔离、审批并记录
适用场景
- 01找出仅由单个测试覆盖的分支
- 02验证覆盖相同的测试是否可替代
- 03调查从未失败或经常波动的测试
- 04安全清理并整理回归测试集
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ petrkindlmann/ qa-skills --skill test-suite-curation安装后在 Claude Code 中输入/test-suite-curation确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/audit-record.mdci-history-mining.mdclustering.mdcoverage-fingerprinting.mddestructive-safety.mdmutation-confirmation.mdtiering.md详细介绍
快速导航
| 你想要…… | 前往 |
|---|---|
| 找出每个测试独有覆盖的行/分支 | 覆盖指纹 |
| 判断“覆盖相同”是否意味着“删掉一个” | 覆盖率相同的陷阱 |
| 找出复制粘贴的近似重复测试 | 近似重复项聚类 |
| 找出从未失败和总是偶发失败的测试 | 挖掘 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 个发布周期)。
核心原则
-
覆盖率相同不等于冗余。 两个测试执行了相同的代码行,也可能断言完全不同的内容——使用不同的判定依据、输入和边界情况。行覆盖率/分支覆盖率只能说明代码运行过,无法说明代码经过了哪些检查。证明一个测试被另一个测试取代的唯一依据,是保留下来的测试能够捕获相同的故障;这需要通过变异测试来证明,而不是比较覆盖率差异。
-
Agent 的优势在于分析整个测试集,而非决定删除什么。 Agent 可以在几分钟内为 4,000 个测试生成特征、将近似重复项分组,并交叉对照 CI 历史记录——这是人类无法手动完成的工作。这就是它的全部价值。但 Agent 负责提出建议,由人类批准。绝不要让面向整个测试集的分析演变成自动删除整个测试集。
-
删除具有破坏性,必须确保实际可恢复。 “Git 历史里有”不算恢复方案。先隔离(跳过/标记为 xfail,或移至已弃用的测试套件),在规定的观察期内监控是否有缺陷漏网,然后经签字批准再删除。宽限期才是安全保障,而不是提交日志。
-
每种处置方式都应明确区分。 冗余、过时和价值低是三种不同状态,应采取三种不同措施。把它们都归入“删除”这一类,就会导致真正的覆盖率丢失。从未失败过的测试,不等于不会失败的测试。
-
以证据而非直觉为准,并为每个测试记录证据。 每次删除都要有对应记录:类别、取代它的测试、覆盖率变化、批准人以及恢复方法。如果无法填写这条记录,就不能删除该测试。
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. 近似重复测试聚类
目标:找出复制粘贴的测试,而不是把同一文件中的每个测试都标记出来。按文件名对测试分组不等于聚类——它无法说明相似度。可结合以下两个可靠信号:
-
AST(抽象语法树)相似度。 将每个测试解析为 AST,对标识符名称和字面量进行归一化处理,然后比较结构。使用 token/tree 相似度指标(对归一化 token shingles 使用 Jaccard,相对于 AST n-gram 使用余弦相似度,或使用树编辑距离)。基于 AST 的比较不会受到格式和变量名差异的干扰,而这些差异会影响精确字符串匹配或原始
diff。切勿使用原始行号作为相似度信号。 -
覆盖率配置签名。 根据 §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 个测试,也不是随机抽样,而且不得仅按每个测试的运行速度分层。分层应依据两项证据:
- 业务风险 / 关键路径 — 来自风险图(
risk-based-testing)。保护收入、身份验证、数据完整性和主要用户旅程的测试,无论速度如何,都应归入 smoke/core。 - 缺陷检测历史 — 实际上发现过真实 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 测试文件。 删除具有破坏性;应设置门槛:
- 先隔离,再删除 — 绝不要直接删除。 将标记出的测试设为跳过(
@pytest.mark.skip(reason="curation-2026-Q2, see audit row")、xfail、Jest.skip),或将它们移至仍保留在仓库中的quarantine//deprecated/测试套件。这样它们不会运行,但仍可见,也能立即恢复。 - 必须获得人工批准。 指定的审批人必须批准,可通过测试目录中的 CODEOWNERS 指定,也可在 PR 中明确指定审阅者。不要以“冗余测试无需审查”为由跳过。直到 §2 确认之前,冗余都只是一个假设。
- 设定观察期。 在宽限期内运行隔离该测试群组的套件 — 默认 2 个冲刺 / 2 个发布周期 — 并监控遗漏的缺陷:留意生产事故和 Bug 工单,查看是否出现本应由被隔离测试发现的问题。观察期内若出现遗漏的缺陷,说明测试并非冗余;应恢复测试。
- 只有在此之后,才能分小批次删除,并为每批删除关联对应的审计记录(§8)。
- 确保可以恢复。 记录隔离位置和提交,以便一步恢复/还原任何测试。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 —
.coverageSQLite 上下文查询、按测试构建行/分支集合、唯一覆盖与包含关系计算,以及等效的 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 — 删除日志的完整列架构,以及一条填写示例。