mcpField
T
testkit

testkit

github.com/mimukit测试与质量2203 次安装今天更新
代码仓库

关于 testkit

testkit 是为已有代码补充测试的技能,可按风险和变更情况排列未测试行为,并维护可续用的计划。它要求测试先实际失败再保留,并将缺陷报告出来而不修改源代码。

主要功能

  1. 01审计与覆盖两种模式
  2. 02按风险和变更历史排序
  3. 03维护仓库测试计划台账
  4. 04要求测试先实际失败
  5. 05报告缺陷但不修复源代码
  6. 06不重构难以测试的代码

适用场景

  1. 01为测试稀少的代码库选择首个测试范围
  2. 02为现有项目补充回归测试
  3. 03跟踪延期内容和可测试性阻碍
  4. 04发现未能捕获行为或范围过宽的现有测试

安装

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

$npx skills add https://github.com/mimukit/skills --skill testkit

安装后在 Claude Code 中输入/testkit确认已加载。

详细介绍

testkit

为已经运行的代码打造的测试套件。testkit 会读取没有测试或只有少量过时测试的代码库,对值得覆盖的部分进行排序,选出一个切片,并编写测试;只有在每项测试都已观察到失败后,才会保留它们。

两种模式。audit 会为未经测试的部分排序,只写入一份记录。cover 会编写测试。

它旨在防止的问题

让 Agent 为“这个项目编写测试”,它通常会产出覆盖率表演:一堆逐行照搬实现的测试,断言模拟对象曾被调用,第一次运行就通过,并将当前代码的行为原样固定下来,包括其中的 bug。

它看起来和真正的测试套件一模一样:相同的布局、相同的绿色勾号、相同的覆盖率数字。它比没有测试套件更糟,因为它会永远带来维护负担,却检测不出任何问题,还会交给你一个绿色勾号,随后成为争论中的证据,而它根本无力支撑这种论点。

真正的棕地测试与那种测试之间有两个区别,除非强制要求,否则这两点在结构上都不存在:

  1. 测试经过观察,确实失败过。 如果先写代码再写测试,红灯转绿灯不会自动发生。必须刻意制造失败,否则它根本不会发生。
  2. 预期结果来自实现之外。 如果测试的预期值是从被测函数本身读出来的,那它只是当前行为的照片,而不是对正确行为的主张。

以下每条规则,都是为了让这两点成为强制要求,而非理想目标。

适用场景

“为这段代码编写测试”,且代码已经在运行;“这个仓库没有测试”;“增加测试覆盖率”;“设置测试”;“我应该先测试什么”;“我们的覆盖率太差了”;“/testkit”。

与最接近的工具包的界限:

  • 不用于新开发的测试。 如果请求是为尚不存在的代码或当前正在构建的功能编写测试,请转交给构建技能:使用 implementkit 的 TDD 模式先写测试,或使用其测试后模式在构建完成后立即编写测试。没有 implementkit 时,请说明测试应与构建一起完成。testkit 的基本前提是代码已经存在且正在运行,因此它负责的是事后补充测试。
  • 不是手动 QA 计划。 QA 技能会编写由人工手动执行的步骤。testkit 生成的内容没有任何一项需要人来运行。
  • 不是拉取请求的浏览器验证。 视觉验证技能会驱动浏览器运行一次并截取图像。testkit 编写的规格会由测试命令反复运行。
  • 不是诊断。 调试技能会追查单个症状的根因,也可能生成一个用于复现问题的失败测试。testkit 构建的是测试套件。

它绝不修复,也绝不重构

两条硬性界限,实际操作时都可能让人觉得不妥:

补充测试会发现 bug。testkit 会报告这些 bug,但不会修复。 如果观察到的行为与外部预期相矛盾,不要修改源代码来迎合测试,也不要修改测试来迎合源代码。编写断言预期行为的测试,将其标记为跳过或预期失败,并附上一行说明,然后报告问题。一项发现缺陷后还顺手提交修复的技能,在写报告前就已经替你做出了决定。

有些代码不重构就无法测试。testkit 不会重构它。 不提取接口,不注入依赖,也不做“顺手加个小接缝”。无法测试的代码会作为可测试性阻碍记入记录,并转交后续处理。重构技能中的未测试耦合模式,正是这类发现并附带建议的做法。

testkit 唯一会进行的源代码修改,是失败门槛中的临时变更,而且每项都会撤销。

模式选择

采用第一个符合条件的模式:

  1. 用户指定了模式,例如 /testkit audit 或 /testkit cover src/billing。显式指定始终优先。
  2. 存在台账 → cover。不存在台账 → audit。

如果请求使用 cover,但仓库没有台账,不要退回。内联执行排序,选定一个切片,说明本次运行采用的路径,然后继续。必须等到有调查文档才能编写测试,这属于官僚作风,而且会让人卸载这项技能。

可选的范围参数可缩小任一模式的排序范围,但不会改变写入内容。

台账

每个仓库使用一个文件:docs/tests/NNNN-testplan-<repo>-YYYY-MM-DD.md,日期为其创建日期,完整文件名永久不变。要获取序号 NNNN,检查当前工作树、git worktree list 列出的所有其他工作树,以及 origin/HEAD(运行 git fetch origin 后执行 git ls-tree --name-only origin/HEAD docs/tests/)中 docs/tests/ 下最大的四位前导序号,并加一;如果没有序号,则从 0001 开始。检查这三个来源,因为并行工作树可能会取得相同序号。跳过检查失败的来源,例如没有远程仓库;如果没有 git,则只列出目录。序号按目录计,永不复用。audit 会创建台账。此后的每次运行都通过 testplan-<repo>- 查找台账,无论是否带有前导序号,并在原处更新。范围受限的运行会在同一文件的范围标题下追加内容,绝不会另建文件。

一个文件能让第 N+1 次运行成本更低。对棕地项目的改造无法在一次会话中完成;而技能若为每个切片都新建一份带日期的调查文档,最终只会留下一堆调查文档,却没有任何可恢复的状态。

台账包含:

  • 排序后的未测试范围,标注 ranked against <sha> on <date>;
  • 每次运行覆盖的内容、日期,以及声明数量与实际数量;
  • 延后处理的内容及原因;
  • 可测试性阻碍,即不经结构调整就无法测试的代码;
  • 未经证实的测试,即在本应捕获某项问题的变异测试中依然通过的现有测试;
  • 范围过宽的测试,即在针对某项未被其名称指明的行为所做的变异测试中失败的现有测试。

持久保存且可提交。testkit 绝不提交台账。

如果仓库已有测试规划文档的存放位置或命名约定,则以该约定为准,并说明已遵循该约定。没有可写文件系统(例如基于浏览器的 Agent)时,则在其规范路径下以代码块形式输出台账。

模式:audit

只读。只写入台账,不做其他操作:不创建测试文件,不修改源代码。

1. 推导未测试范围

查明存在哪些源代码、有哪些测试,以及这些测试实际覆盖了哪些内容。只有当覆盖率报告已存在于磁盘上时才读取。绝不生成覆盖率报告,也绝不安装工具来生成证据,因为探测分析工具并解析其输出会让这项技能依赖一种可能在次要版本更新中变化的格式。

排序前先排除,而不是排序后再排除:生成代码、供应商代码树、简单配置和纯委派代码。它们会虚增数量,却无法证明任何事情。

当每个源文件都归入候选列表或某个有名称的排除类别时,此步骤即告完成。

2. 排序

读取一次历史记录,就能同时获得两类信号:git log --format= --name-only --since=<about a year> 会显示每个文件的变更频率,以及哪些文件总是一起变更。

根据四项信号排序:

  • 变更频率。 文件变更的频率。无人触碰的代码,也不会引发故障。
  • 扇入度。 依赖该文件的模块数量。这里一处故障,可能处处故障。
  • 故障代价。 金钱、身份验证、数据丢失、迁移,以及任何不可逆的事项。
  • 可测试性代价。 按此项折减。需要启动三个服务才能测试的行为,成本高于其排名所显示的程度。

没有 git 历史记录,也就是浅克隆或当前目录不是仓库,意味着无法排序。改为按结构扫描,并明确说明跳过了优先级排序,免得有人把覆盖率声明误读为更高的结论。

当每个候选项都有排名,或者此次运行已说明跳过排序及其原因时,此步骤即告完成。

3. 选定一个切片

沿着协同变更的集群,把排名靠前的内容归为连贯的切片,然后只选定一个,并按顺序列出备选项。选定切片,而不是文件:一个连贯的行为很少只存在于一个文件中,而且文件不是意义的单位。

选定一个,就是这项工作的核心。列出五个看起来同等重要的候选项,用户仍会停留在原来的状态。

4. 写入台账

每次运行都要在终端和文件中写入一行覆盖范围说明:“已对 1,240 个文件排序,读取了 3 个切片中的前 40 个。” 如果上限不可见,覆盖范围就会被误认为完整覆盖。

如果审查发现没有有意义的未测试部分,就不写入文件,并明确说明。 被要求必须产出发现的调查会凭空捏造发现;关于测试内容的捏造建议,读起来和真实建议一模一样。

模式:cover

编写测试。

1. 选取切片,并确认排序仍然有效

从台账、用户提供的内容,或在两者都不存在时从内联排序中选取,并说明切片来源。

台账中的排序会标注其计算所依据的提交。比较 HEAD。如果自该 sha 以来的提交修改了台账中排名靠前的切片里的文件,就重新排序。 否则就明确说明所依据的 sha,并在说明中信任该排序。排序可能比它所依据的代码更长久,因此,八月的排序完全可能在十一月驱动一次运行,而那时热点早已改变。

当此次运行已说明切片、切片来源,以及所信任的 sha 或新排序时,此步骤即告完成。

2. 如果没有测试运行器,就搭建一个

如果仓库已有测试运行器,则跳过此步骤。

  • 沿用该生态系统的默认选择。 没有测试的项目,也就没有需要尊重的既有意见;最不容易引发争议的选择,就是该语言官方文档所推荐的方案。如果确实存在争议,就转交给研究技能,而不是在此处决定。
  • 将其接入仓库现有的入口点,例如 npm test、make test 或 pytest,这样用户可以通过本来就会尝试的命令运行测试套件。只能通过聊天记录中的一串咒语才能运行的测试套件,不算测试套件。
  • 测试文件的放置遵循同一规则。 如果某个生态系统存在两种常见惯例(测试文件与代码同目录,或使用 tests/ 目录),就遵循仓库现有布局所体现的惯例,而不是引入个人偏好。
  • 安装依赖前只询问一次。 开发依赖是对他人项目的一项持久变更。
  • 开始编写任何实质性测试之前,先让一个绿色冒烟测试通过。 如果改造一开始就针对未经验证的测试框架编写四十个测试,就会让测试承担调试框架的工作。

如果无法接入标准测试运行器,因为语言、构建系统或依赖情况无法支持,就报告具体障碍并停止。绝不要临时拼凑测试框架。 手工编写的测试循环,别人无法运行、维护或替换,而且可能成为这项技能留下的最持久产物。

当一个冒烟测试通过仓库入口点运行,或此次运行因明确说明的障碍而停止时,此步骤即告完成。

3. 编写任何内容前先声明数量

在第一个测试文件出现之前就说明:“本部分有 N 项行为,本次运行覆盖 M 项。”

数字由你决定。它们会预先记录下来,交接时会报告实际数量与声明数量的对比。若本次运行想超出声明,就停止并说明原因,而不是悄然偏离。

除此之外,没有任何内容限制一次运行的范围。事后看来,三个测试和三十个测试都符合要求,而失败门槛的成本会随着一个无人记录的数字线性增长。固定上限不适用于 200 行的实用工具和支付模块;每次运行都要提问,又会导致无人值守的运行无法进行。

4. 编写测试

每项预期都必须来自实现之外,也就是来自文档字符串、README、问题描述、类型签名、调用方的实际用法或领域知识。如果外部没有依据,测试就是特征测试:用于锁定当前行为的变更检测器,不对行为是否正确作出判断。

每个测试都必须附带一行来源注释,无论属于哪种类型:

// per README: "amounts are stored in cents"
// characterization: no docstring, no issue, no caller assertion

让两种做法的成本相同,就是整个机制的关键。免费的标签会被贴到所有内容上,直到这种区分失去意义。若某个标签比替代做法更费力,就会促使人编造外部预期来规避它,而这正是这条规则要揭穿的不诚实行为。两种做法都要求注明依据,就能杜绝双向规避。

只在进程边界处使用伪造对象,即网络、时钟、随机性、文件系统或第三方服务,并且只有在真实对象不可用或行为不确定时才使用。绝不要伪造边界内的协作者。**绝不要断言某次调用确实发生。**如果测试唯一的断言是调用次数或间谍对象的参数,那么它检查的只是实现是否就是这个实现。

每个测试只验证一项行为,名称应描述该行为,而不是它恰好通过哪个函数执行。

遇到矛盾时,应跳过测试并提交报告,绝不能修改源代码,也不能放宽断言。参见它从不修复,也从不重构。

当 M 项已声明行为都各自有一个带来源注释的测试,或有一个跳过的测试和一份报告时,这一步就完成了。

5. 对每个目标进行验证后再运行

任何测试都不得针对尚未由 testkit 验证为可丢弃的数据存储或服务运行。

符合条件的目标:localhost、容器 testkit、用户启动的容器 testkit,或明确指定的测试 URL。**从环境中的 .env 继承的值永远不符合条件,不论该文件叫什么名字。**名为 test 的配置只是一个声明,不是事实;相信它,正是会毁掉他人数据的假设。

如果没有可丢弃的目标,只询问一次是否要搭建一个。如果答复是否,就把集成工作作为阻碍事项记入台账,并继续进行单元测试层级。

这是这里唯一一条违反后无法挽回的规则,所以它采用拒绝而非提醒的措辞。

6. 适用时执行端到端测试层级

选择加入,范围从窄。只有当应用已能以非交互方式启动,并且驱动程序已安装或用户批准安装时,才运行此层级。将其限制在少数关键路径测试中,例如登录和那笔重要交易,绝不要照搬整个 UI。

**testkit 绝不会将打开浏览器作为会话操作。**它负责编写测试规格;唯一会驱动浏览器的,是仓库自己的测试命令执行该测试规格。因此,失败门槛对端到端测试规格的要求与单元测试完全相同:必须通过运行器执行,而这个最容易写错的层级也没有例外。

这就是它与视觉验证技能之间的界限。这里明确规定的是谁来调用驱动程序,而不是产物的生命周期,因为两种技能都合理地需要相同的启动命令、相同的固件和相同的驱动程序配置;只有前一种界定方式能经受住这种重叠。

失败门禁

testkit 保留的每个测试都必须观察到过失败。 从未观察到失败的测试,无法证明它是否与代码存在任何关联。

代码已经存在,因此无法采用先红后绿的方式。替代方案是:破坏该行为、运行测试、观察它失败,然后恢复。

有效的破坏是语义变更,绝不能是删除。 更改返回值、反转比较、移除分支、跳过写入。删除函数或文件会导致所有内容都失败,包括断言任何内容的测试,因此只能证明导入路径有效,除此之外什么也证明不了。只做最小的修改,改变测试声称要检查的行为。

运行测试运行器支持的最小范围。 目标测试以及同一文件中的其他测试。绝不运行完整测试套件。确认预期的测试变红,然后检查所有同样变红的相邻测试。如果运行器无法按文件或模式选择测试,就降低声明的行为数量,并说明原因。

变红的相邻测试只有两种情况,门禁会将它们区分开。

  • 有效重叠。 相邻测试自身所命名的行为依赖于被修改的代码,因此它理应失败。两个测试都检查同一个定价函数,反转舍入逻辑时两个测试都会失败。保留两个测试,并在变更记录行中记录这一重叠。
  • 断言范围过宽。 相邻测试命名的是另一种行为,而它之所以失败,是因为断言超出了该行为的范围:断言整个对象、完整快照或无关字段。testkit 在本次运行中编写该测试时,应将断言收窄到其命名的行为,并重新运行该测试的门禁。对于预先存在的测试,在台账中将其记录为范围过宽,不要改动。

判断依据是相邻测试的名称和来源注释与该变更之间的关系:如果该变更仍然生效时,测试所命名的行为就不可能正确,那么这种重叠就是有效的。

绝不能因为耗时而跳过门禁。 测试套件缓慢,恰恰是门禁最有价值的时候;豁免会在最容易让人偷懒跳过门禁的情况下放开限制。完整测试套件只在完成门禁时运行,且恰好运行两次,不得在其他时候运行。

testkit 在本次运行中编写、且在针对它的变更下仍然通过的测试,必须删除,并报告删除情况。

预先存在且仍然通过的测试应记录,而不是删除。 在台账中将其记录为未经验证,注明未能触发该测试的变更,并将其转交给用户。删除规则仅适用于 testkit 在本次运行中编写的测试。根据针对其他内容的变更所得到的证据,删除他人编写的测试,超出了此技能获准执行的范围,也会把有帮助的信号变成无法挽回的损失。

变更账本

变更是对受跟踪源文件的编辑。运行开始时,用户可能有尚未提交的工作;还原变更绝不能还原这些工作。

  • 先建立基线。 将账本保存在工作树之外的目录中。首次变更前,运行 git stash create,并在账本中记录 git rev-parse HEAD、git diff --binary --cached 和 git status --porcelain=v1 --untracked-files=all 的结果。
  • 记录快照是否存在。 git stash create 会为受跟踪的变更创建一个没有引用的提交,不会触及任何引用或文件。如果工作树干净,或唯一的变更是未跟踪文件,它不会输出任何内容。此时写入 none,且绝不要为不存在的对象输出恢复命令。快照存在时,在交接说明中输出其 SHA 和对应的 git stash apply <sha> 恢复命令。Git 会按自身的计划清理无法访问的对象;应如实说明这一点,不要夸大快照的持久性。
  • 单独保留未跟踪文件。 快照不包含这些文件。变更触及未跟踪源文件前,先将其复制到账本中。
  • 每项变更都单独记录为针对变更前状态的补丁。 每次变更前,将文件复制到账本中的新 mutation-NN/ 目录。变更后,将该副本与文件进行差异比较。首次变更某个文件前,还要保留一份 .base 副本。针对旧副本生成的补丁会包含更早的变更,反向应用时会将它们重复还原。每项变更都要记录一行,包括文件、补丁以及该变更要验证的行为。
  • 使用 patch -R 反向应用记录的补丁来还原。绝不要恢复文件。 git checkout -- <file> 是人们下意识会用的做法,也是会悄无声息地破坏已有未提交编辑的做法。此禁令没有任何例外,即使文件在基线时看起来是干净的也不例外;凡是需要考虑例外的禁令,都不是真正的禁令。
  • 反向应用发生冲突时,停止运行并报告。 不要强行处理,也不要改用恢复文件的方式。
  • 宣布完成前,验证基线。 每个已变更文件都必须与其 .base 副本一致;HEAD 和暂存区必须与记录一致;未跟踪文件列表必须一致,但 testkit 创建的测试文件除外。每个被变更触及的未跟踪文件都必须与账本中的副本一致。按路径说明任何有意保留的文件。
  • 绝不要还原、暂存或丢弃 testkit 未创建的变更。

完成门槛

“完成”意味着测试通过、连续通过两次、顺序打乱且工作树干净。应从仓库自身发现命令(package.json 脚本、Makefile、pyproject.toml、justfile、CI 配置),不要猜测。

  • 新测试套件通过。
  • 每个保留的测试都必须针对某个明确命名的变更实际观察到失败。
  • 测试套件必须连续运行两次,且两次都通过;如果运行器支持随机顺序,则应打乱顺序。顺序依赖是补充测试套件最典型的不稳定特征,因为针对现有共享状态编写的测试会继承该状态。
  • 仓库自身的构建/类型检查仍须通过。
  • 源代码树中不得残留任何变更。

如果门槛未通过,修正自己的输出并重新运行,尝试次数控制在大约三次以内。然后停止。门槛未通过时绝不要宣布完成,也不要反复循环。此时最诱人的做法是放宽断言,但这不是修复。

覆盖率数字

覆盖率数字从来不是目标,也不是停止条件。 只有工具链本来就会生成覆盖率数字时,才能将百分比作为前后对比事实报告。

停止条件是声明的计数。目标百分比是制造本技能旨在防止的问题堆积最可靠的方式:上述每条规则都要付出代价,而即使不遵守其中任何一条,也能达到某个数字。

交接

本节使用操作性语体:每句话只给出一条指令,使用主动语态和现在时,不使用比喻。

audit 之后。 报告覆盖率摘要行、排名第一的切片和按顺序排列的其他候选项。给出账本路径。列出本次运行发现的可测试性阻碍,以及尚未证实的既有测试(如果有)。说明目前还没有测试代码。然后确定一个下一步:对排名第一的切片运行 cover。如果 audit 没有写入文件,就说明该范围已经覆盖,并指出没有下一步。

cover 之后。 报告:

  • 模式,以及切片的来源,无论是账本、用户还是内联排名;
  • 声明与实际情况,例如“声明有 8 项行为,覆盖了 8 项”;
  • 创建和修改的文件,包括运行器接线;
  • 门禁结果,即运行过的命令、每项保留的测试都曾观察到失败,以及测试套件连续两次通过;
  • 删除项,即 testkit 编写后又移除、因为一直保持通过状态的测试;
  • 按路径列出尚未证实和范围过宽的既有测试;
  • 基线快照:baseline snapshot: <sha> · recover with git stash apply <sha>,或 baseline snapshot: none (no tracked change at start) · mutation patches in <ledger>;
  • 工作树状态,即每项基线检查的结果:已变更文件、索引和未跟踪文件,或仍带有变更的路径。

所有变更都保持未暂存。不要运行 git add。不要提交。不要起草提交消息。

然后确定一个下一步:

  • 账本中仍有待处理工作 → 对下一个切片再次运行 cover。
  • 本次运行发现矛盾 → 如果已安装调试技能,就转交给该技能;否则明确说明需要有人判断行为还是预期有误。
  • 本次运行发现可测试性阻碍 → 如果已安装重构技能,就转交给该技能;否则说明代码需要先提供一个测试接缝。
  • 没有剩余工作 → 说明该切片已完成,并指出下一步是提交。建议用户运行类似 test(<area>): cover <slice> 的提交消息。绝不自动提交。

注意事项

  • 绝不粉饰通过结果。 声明“完成”始终意味着门禁确实通过。有限修复后仍然失败,就如实报告失败。
  • 永久源码变更为零。 测试文件、运行器接线和账本构成全部写入范围。每项变更都是临时的,之后都会还原。
  • 只读分析。 查看 Git 历史、读取文件、执行 grep。绝不安装或运行分析工具来生成证据。只读取磁盘上已有的产物;绝不生成产物。
  • 转交,不启动。 指明下一个工具包及其单行调用方式;不要调用它。
  • 转交,不强制要求。 如果未安装所指明的工具包,就将每项建议转为普通操作。在只有 Git 的空仓库中,testkit 也有用。
  • 以仓库规范为准。 如果仓库中已有既定的测试布局、文档化约定或 agent-guide 文件(CLAUDE.md 或同等文件)中说明的策略,应遵循该规范,并说明你已遵循。
  • 没有文件系统或 shell? 你无法写入文件、运行门禁或更改任何内容,因此无法运行失败门禁,也不得将任何测试说成已验证。将建议的测试连同其路径放在围栏代码块中打印,将账本作为代码块打印在其规范路径下,并列出用户必须自行运行的门禁命令。

评论

支持 Markdown