mcpField
T
test-driven-investigation

test-driven-investigation

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

关于 test-driven-investigation

一项通过尽早编写并运行最小复现测试来调查 Bug 的技能,再依据测试结果推进分析。面向 workerd 及相关代码库的开发者,涵盖测试选择、提交历史检查和验证测试路径。

主要功能

  1. 01尽早编写最小复现测试
  2. 02依据测试结果推进调查
  3. 03检查相关提交历史
  4. 04尽可能改造现有测试
  5. 05验证测试覆盖目标代码路径

适用场景

  1. 01调查崩溃或断言失败
  2. 02验证近期改动相关的假设
  3. 03用聚焦测试复现 Bug
  4. 04确认通过的测试实际执行了相关功能

安装

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

$npx skills add https://github.com/cloudflare/workerd --skill test-driven-investigation

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

详细介绍

测试驱动调查

概述

调查错误时,应尽早编写一个能复现问题的测试。不做实验的分析很容易陷入循环推理。

核心原则: 一个 20 行、会失败的测试,比花 2 小时阅读源代码更有价值。

反面模式

READ code → BUILD mental model → READ more code → REVISE model → READ more code →
SECOND-GUESS model → READ same code again → ... → eventually write a test

这看起来很有效率,实则不然。你只是在代码中寻找模式,却没有以现实情况为依据。每重新阅读一遍,增加的都是不确定性,而非清晰度。

正确模式

1. READ the error message / assertion / crash
2. IDENTIFY the minimal trigger (what operation, on what object, in what state?)
3. WRITE a test that sets up that state and performs that operation
4. RUN it
5. Let the RESULT guide the next step
6. Iterate as needed, but always grounded in test results backed by code, not just code.

步骤 1–2 应该花几分钟,而不是几小时。你不必理解完整的调用链才能编写测试,只需知道入口点是什么,以及哪里出了问题。

熟悉情况

编写测试前,你需要了解三件事:要测试哪个对象或 API、测试文件应放在哪里,以及如何构建和运行测试。最多花 20–30 次工具调用来查找这些信息。这是为了编写测试而进行的有限调研,而不是漫无边际地分析代码。如果你不清楚确切的 API,就选最接近的那个,照样编写测试。即使测试使用了错误的 API 却通过了,也能说明一些问题。

检查提交历史

调查初期,检查近期有哪些提交改动了相关代码。最近出现的错误,很可能也是最近引入的。运行 git log --oneline -20 -- path/to/relevant/files 只需几秒,就能立即将搜索范围从“某处出了问题”缩小到“这个特定改动可能是原因”。

这不能代替编写测试,而是帮助你更快地提出更好的假设。如果你能找出可疑提交,第一个测试就应尝试确认该改动是否是原因。

找到可疑提交时: 不要就此止步。务必检查它前后至少各 10 个提交。错误往往由多个改动之间的相互作用引入,而非单个提交。单独看来无害的提交,可能破坏了附近某个提交所依赖的假设。查看周边提交也能避免确认偏误——你可能会在相邻改动中找到更合理的解释。

不要把相关性误认为因果关系。 某个提交的时间恰好与错误出现的时间一致,这只是线索,不是结论。这可能纯属巧合——真正原因可能是环境变化、依赖更新、仅在新的负载模式下更容易发生的竞态条件,或者被无关改动触发的潜在错误。应将可疑提交视为需要验证的假设,而不是有罪证据。如果你无法证明该提交导致错误的机制,就还没有找到原因。

需要关注的内容:

  • 崩溃或断言发生处的文件或函数改动
  • 受影响子系统的设置、配置或初始化代码改动
  • 受影响代码所使用的共享工具或基类改动
  • 对该区域中内容的移动或重命名进行重构

限制调研范围: 大约用 5 次工具调用执行 git log 和 git show 即可,不要把它变成考古之旅。如果没有明显线索,就继续前进,根据已有信息编写测试。提交历史只是形成假设的依据之一,并非前置条件。

当你忍不住想读更多代码时

问问自己:

  • “我读这些代码是为了编写测试,还是为了逃避编写测试?” 如果你说不清测试应该是什么样,那就该解决这个问题,而不是继续读更多代码。
  • “我有可以验证的假设吗?” 如果有,就验证它。如果没有,就尽可能提出一个简单的假设并验证。
  • “我有不止一个假设吗?” 要么选一个来验证,要么并行验证多个。别陷入“我必须先理解一切,才能开始测试”的想法,也别试图在脑中同时权衡多个思维模型。
  • “我是不是在重读已经读过的代码?” 停下来。你卡住了。根据当前的理解编写一个测试,即使理解有误也没关系。一个能运行的错误测试也能让你学到东西;一个从不验证的正确心智模型则什么也教不了你。
  • “我是不是在重复走同一条路径?” 如果你发现自己多次跟踪同一个调用栈,就停下来。编写一个能直接触发故障点的测试,之后随时可以调整。如有必要,可以用一份临时跟踪文档记录已经读过的内容,这样就不必全记在脑中,也不必重复阅读相同的代码。但这份文档永远不能比编写并运行测试更优先。

从现有测试入手

从头编写测试之前,先找找同一功能或子系统的现有测试。搜索能够覆盖你正在调查的 API、协议或代码路径的测试,而不只是崩溃位置所在文件中的测试。

现有测试蕴含着无法仅靠阅读源代码获得的隐性知识:必要的设置、框架特定的验证模式、配置细节、关停处理方式。结构有误的测试(缺少验证、配置错误)可能会悄无声息地“通过”,实际却什么也没测到。

修改现有的可用测试,而不是凭空编写一个。 如果现有测试适用于该功能,而 bug 受某个标志、自动门控或配置变更控制,那么最快的复现方法往往是克隆这个可用测试,只修改一个变量。

确认测试确实覆盖了代码路径

测试通过并不能证明功能生效了,也可能意味着功能根本没有运行。

每次运行测试后,都要检查输出,确认特定代码路径确实被执行的证据——例如提及该功能的日志行、发出的子请求、预期的错误消息或记录的指标。如果在测试输出中找不到证据,那么无论退出状态如何,该测试都无效。

具体检查项:

  • 测试输出是否提到了该功能的脚本、工作线程或管道名称?
  • 日志中是否能看到预期的子请求或 RPC 调用?
  • 如果该功能会产生副作用(追踪数据交付、存储写入、网络调用),这些副作用是否可观察到?
  • 如果完全移除该功能的配置,测试是否仍会通过?如果是,那么这个测试就没有在测试该功能。

确定测试范围

你不必复现完全相同的生产场景,只需复现其_机制_。

生产环境中的崩溃: KJ_REQUIRE(!writeInProgress, "concurrent write()s not allowed")

你不需要: 生产环境调用栈中可能导致此问题的每个细节。

你需要: 找出触发它的最短路径,即使这条路径只是理论上的。

Good test scope:
  Create the pipe/adapter/object directly
  → Put it in the suspect state
  → Perform the operation that should fail
  → Assert what happens

Bad test scope:
  Reproduce the entire production call stack
  with all middleware and wrappers

提出假设

调查 bug 时,假设可以是:

“如果我在 Y 失败后执行 X,就会发生 Z,因为 Y 的错误处理路径中的清理操作没有执行 W。”

它不必是:

“我已经追踪了每一条代码路径,并确信第 847 行是根本原因,因为它与……发生了相互作用。”

第一个版本几分钟内就能验证。第二个版本需要花上几小时才能构建,而且最终仍可能是错的。

测试之后

  • 测试按预期失败: 你已确认了 Bug 的触发机制。现在可以_有目的地_阅读代码——寻找修复方法,而不是寻找 Bug。
  • 测试通过(未复现 Bug): 你的假设错了。这很有价值。调整假设后再试。这比再花一小时阅读代码更快。
  • 测试以不同方式崩溃: 你发现了其他问题。顺着这条线索继续调查,但不要放弃最初的工作。你可以并行调查多个方向,但每个方向都应以测试结果为依据,而不只是阅读代码。

危险信号——你分析过度了

  • 你读同一个文件或函数超过两遍
  • 你正在构建“谁调用谁”的多层心智模型
  • 你还没写任何测试代码,就在详细记录代码的执行流程
  • 你不止一次地说“我先弄明白 X,再写测试”
  • 你觉得必须了解整个系统,才能测试单个组件
  • 测试“通过”了,但你没有证据表明功能确实运行过——你却在读代码解释原因,而不是修正测试
  • 你写了多个测试,它们都通过了,却没有复现 Bug,而你还没质疑这些测试是否走了正确的代码路径

构建时间不会改变优先级

在这个代码库中,C++ 编译并运行测试可能需要几分钟。这不能成为推迟测试、转而阅读更多代码的理由。它只会改变“快速反馈”的含义:

  • 编译速度快的代码库: 编写测试、运行测试、几秒内查看结果,然后快速迭代。
  • 编译速度慢的代码库: 编写测试、启动构建,并利用等待时间有针对性地阅读代码,为下一轮迭代做准备。构建正在运行——你并未被阻塞,而是在并行处理。

慢速构建容易让人产生“我应该彻底确认后再编译”的想法。这只是分析螺旋的伪装。首次未能复现 Bug、但已成功编译并运行的测试并非白费功夫——它是一个经过验证的测试框架,你可以在下一轮调整它,而且增量重建通常会快得多。

应用到此代码库

workerd 及其依赖项(KJ、Cap'n Proto)提供了完善的测试基础设施:

  • KJ 测试: 在 *-test.c++ 文件中使用 KJ_TEST("description") { ... }
  • workerd 测试: 使用 .wd-test 格式编写 JS/TS 集成测试
  • 构建/运行: bazel test //path:target --test_arg='-f...' --test_output=all

大多数 KJ/capnp Bug 都可以用一个调用公共 API(管道、流、Promise、HTTP)的独立 KJ_TEST 复现。你很少需要访问内部实现。

归根结底

编写测试。运行测试。分析结果。思考。迭代。

不要反过来。

评论

支持 Markdown