教程Claude Code系列第 2 / 6 篇

用 Claude Code 给老代码补测试:先找缺口,再让每个测试证明自己有用

让 AI 补测试,最怕补出一堆只为凑覆盖率的用例。本站在 Express 的源码上实测了一个两步的做法:先让它找出没测到的代码并按风险排序,再补测试,并要求每个用例都通过“把代码改坏、确认测试会失败”的验证。附真实过程、它自己纠正的一处误判和我们的复核。

发布 · 约 10 分钟读完 · 经人工审核()· 本站的内容怎么来的

本站已实测,测试日期:2026-10-11。

本页目录(10 节)
  1. 起点:一个测试已经很全的项目
  2. 第一步:只分析,不写代码
  3. 第二步:补测试,并要求每个用例证明自己
  4. 结果
  5. 我们的复核
  6. 为什么要这样要求
  7. 哪些不要硬补
  8. 可以直接用的提示词
  9. 这一趟花了多少
  10. 这次实测没有覆盖到的

这篇教程解决一个问题:给已有的代码补测试,怎么保证补出来的是有用的测试,而不是一堆只为让覆盖率好看的用例。

让 Claude Code “给这个文件加点测试”,它一定会交出一批全绿的用例。问题在于,全绿说明不了什么:一个什么都不检查的测试也是绿的。

这篇用的办法分两步:先只分析、不动手,把没测到的代码找出来并排个轻重;再补测试,并且要求每个新用例都证明自己能发现问题。

实测环境
日期 2026-10-11
版本 Claude Code 2.1.296
系统 Ubuntu 24.04
模型 Opus 5.5,auto 模式
连接方式 通过酷站AI 中转站,不是直连官方
示例仓库 Express 的源码,v5.3.0,没有做任何改动

起点:一个测试已经很全的项目

Express 自带 1261 个测试。先看覆盖率的基线:

git clone --depth 1 --branch v5.3.0 https://github.com/expressjs/express.git
cd express
npm install
npm run test-cov

lib/ 目录的结果:

File             | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
All files        |   98.75 |    96.21 |     100 |   99.61 |
  request.js     |     100 |    98.11 |     100 |     100 | 273
  response.js    |   97.08 |    93.71 |     100 |   99.08 | 827,831,835
  view.js        |     100 |    96.15 |     100 |     100 | 53

语句覆盖 98.75%,看起来没什么可补的。正因为这样它适合做例子:剩下的都是不显眼的缺口。你自己的项目缺口只会更多,做法是一样的。

1只分析,不写代码

跑一下 npm run test-cov,找出 lib/ 下没有被测试覆盖到的行和分支。每一处用一两句话说明:
这段代码在什么情况下才会执行,漏测它有什么风险。按风险从高到低排。先不要写任何代码。

1 分 16 秒后,它给出六项。整理成表:

排序 位置 什么时候才会执行 漏测的风险
1 response.js 里发送文件的函数,930 到 979 行多处 文件传到一半客户端断开,或者几个事件几乎同时触发 这是防止回调被执行两次的逻辑。失效了会出现“响应头已发送”的报错,或者把客户端断开误报成错误
2 response.js:642,res.append() 给同一个响应头连续追加值,最常见的是写多个 cookie 出问题时前面写的 cookie 会被悄悄丢掉,登录态就坏了,而且不报错
3 response.js:826、830、834,res.redirect() 参数不对:没传地址、地址不是字符串、参数顺序写反 这几处会发出弃用警告。没人验证过警告真的会发出来
4 request.js:273,req.is() 调用时直接传一个数组 文档里写明支持的用法,但逻辑很简单,出错的可能性低
5 response.js:295,res.jsonp() 正常情况下不会执行 很低。是一个防御性的分支,实际走不到
6 view.js:53 直接创建 View 对象又不传选项 最低。Express 内部调用时总会传

这份清单有两点比覆盖率报告本身有用。

一、它找到了报告没列出来的缺口。 终端里的报告只列了 273、827,831,835、53 五处。它的说明是:

终端的文本报告只列出了 request.js:273、response.js:827,831,835、view.js:53 这几行,但实际漏测的不止这些。
sendfile() 里有好几处 if (done) return; 写在一行里,if 判断执行过,那一行就算覆盖了,但 return 本身从来
没执行过。我另外导出了 JSON 报告逐条核对了一遍。

排在第一位的那一项,就是这样找出来的。

二、它区分了“没测到”和“值得测”。 第 5 项它直接说了实际走不到,第 6 项说了只影响极少数用法。覆盖率数字不会告诉你这些。

先分析不动手,是官方文档反复建议的做法:把调查、计划和动手分开,免得它一上来就去解决错的问题。这一步的结果你可以改:觉得排序不对、有的不用补,现在说,比它写完再推翻省事。

2补测试,并要求每个用例证明自己

我们决定先补第 2、3、4 项。第 1 项要模拟连接中断,另算。

先补第 2、3、4 项,第 1 项先不做。要求:
1)照 test/ 里对应文件现有的写法,只测对外的行为;
2)不要改 lib/ 下的任何代码;
3)每个新用例都要做一次验证:临时把它要保护的那行代码改坏,确认这个用例会失败,再把代码恢复;
4)最后重跑 npm run test-cov,告诉我覆盖率前后的变化。
用 Edit 工具改文件,不要提交。

第 3 条是这篇的核心。一个测试有没有用,最直接的检验是:它要保护的代码坏了,它会不会失败。

这一轮用了 5 分 25 秒。它做的事情,按顺序:

先读现有的测试,再探明真实行为。 写 redirect 的断言之前,它先写了一个临时脚本,看参数不对时到底会发生什么:

● Existing tests pass. Before writing the redirect tests I'll check what res.redirect() actually does with bad
  arguments, so the assertions match real……

写完之后,发现自己上一步有一处判断错了。 它对新加的用例单独跑了一次覆盖率,结果和预想的不一样:

● I found the real uncovered case: line 642's [prev].concat(val) handles when the header holds a single string
  and an array is appended, not the "already an array" case I thought earlier. Let me check whether my new tests
  actually cover this arm.

● The array-onto-array case was already covered, but "prev is a string, val is an array" wasn't hit by either
  new test. I'll swap the second test to cover that case……

第一步里它说第 2 项漏测的是“响应头已经是数组时再追加”。实际漏测的是另一种情况:原来只有一个字符串值,再追加一个数组。它换掉了一个用例,并在最后的总结里把这个更正放在了最前面。

如果没有“重跑覆盖率”这条要求,这个错不会被发现:新测试照样是绿的,只是没有测到真正的缺口。

然后逐个改坏代码。 以其中一次为例:

● Mutation A1: make prev.concat(val) drop the new value. This should fail the three-appends test.

● Update(lib/response.js)
      641 -    value = Array.isArray(prev) ? prev.concat(val)
      641 +    value = Array.isArray(prev) ? prev

● Bash(… npx mocha --require test/support/env -R dot test/res.append.js …)
  ⎿    6 passing (112ms)
       1 failing

           AssertionError [ERR_ASSERTION]: should have 3 occurrences of "Set-Cookie"

● A1 fails as expected. Restoring it, then mutation A2……

这样的验证一共做了 6 次,每次都是改坏、确认失败、改回来。最后它用 git diff lib/ 确认结果为空。

结果

它最后给的表(原文是表格,这里照录):

文件 用例 验证时改坏的地方 结果
test/res.append.js 响应头已有两个值时再追加一个 把 prev.concat(val) 改成 prev 失败:期望 3 个 Set-Cookie,实际 2 个
test/res.append.js 只有一个值时追加数组(真正漏测的那一支) 把 [prev].concat(val) 改成 [prev] 失败:期望 3 个,实际 1 个
test/res.redirect.js 不传地址时发出警告 if (!address) 改成 if (false) 失败
test/res.redirect.js 地址不是字符串时发出警告 对应的判断改成 if (false) 失败
test/res.redirect.js 参数顺序写反时发出警告 对应的判断改成 if (false) 失败
test/req.is.js 传数组时返回第一个匹配的类型 if (!Array.isArray(types)) 改成 if (true) 失败:期望 "application/json",实际 false

覆盖率的变化:

语句 分支 函数 行
之前 98.75% 96.21% 100% 99.61%
之后 99.12% 97.47% 100% 100%

它的总结里还有两句话,说明它在判断什么该测、什么不该测:

- redirect 的测试只检查警告有没有发出。……参数非法时 redirect 后续的行为本身就很奇怪:比如不传 url 会得到
  Location: undefined,参数写反会返回 500。这些属于碰巧的结果,我没有把它们写进断言,免得测试锁死这些行为。
- 第一个 append 用例(三次追加)覆盖的那一支原来已经被测到过,不算填补漏测,但它明确验证了连续写三个
  cookie 的情况。如果你觉得多余,可以删掉。

第一句是写测试时很容易犯的错:把代码“现在恰好这样”的行为写成断言,以后谁改好了它,测试反而会挂。

新加的用例长这样,和文件里原有的写法一致:

describe('when given an array of types', function () {
  it('should return the first matching type', function (done) {
    var app = express()

    app.use(function (req, res) {
      res.json(req.is(['text/html', 'application/json']))
    })

    request(app)
    .post('/')
    .type('application/json')
    .send('{}')
    .expect(200, '"application/json"', done)
  })
})

我们的复核

它报的数字,我们退出会话后自己跑了一遍:

它说的 我们跑出来的
1267 个测试全过,比原来多 6 个 1267 passing
lib/ 没有改动 git diff --stat lib/ 为空,改动只有三个测试文件,共加了 123 行
覆盖率 99.12% / 97.47% / 100% / 100% 一致

我们还自己做了一次“改坏”:把 request.js 第 273 行的判断改成 if (true),再跑 test/req.is.js,结果是 11 个通过、1 个失败。新加的那个用例确实拦得住。

为什么要这样要求

把这次实测里起作用的做法归纳一下,前两条是官方文档明确建议的,后三条是这次试出来的。

做法 作用
要测什么行为说具体 官方文档:要测试时,明确说你想验证的是什么行为
让它照现有测试的写法 官方文档:它会去读已有的测试文件,沿用同样的风格、框架和断言方式
先分析再动手 你有机会在它写之前改排序、砍掉不值得测的
改坏代码,确认测试会失败 筛掉那些永远是绿的、什么都不检查的用例
重跑覆盖率,报告前后变化 确认新测试打到了要补的地方。这次它靠这一步纠正了自己的误判

官方文档还有一条相关的建议:可以让它帮你找漏掉的边界情况,它会分析代码路径,提出错误条件、边界值、意外输入这些容易被忽略的测试。第一步做的就是这件事。

哪些不要硬补

覆盖率不是目标。这次我们留下了三项没有补:

  • 第 1 项(连接中断时的处理)风险最高,但最难测。 要在测试里让客户端在传输中途断开,或者模拟多个事件同时触发。这类测试容易写得不稳定,值得单独花时间做,不适合和简单的用例混在一起一次交给它。
  • 第 5 项实际走不到。 为了覆盖率去测一个不可能发生的分支,是浪费。
  • 第 6 项影响极小。

哪些该补、补到什么程度,是你的判断。它能把依据摆出来,决定要你来做。

可以直接用的提示词

第一步:

跑一下覆盖率(命令是:……),找出 …… 目录下没有被测试覆盖到的行和分支。每一处说明:
这段代码在什么情况下才会执行,漏测它有什么风险。按风险从高到低排。先不要写任何代码。

第二步:

补第 …… 项。要求:
1)照现有测试文件的写法,只测对外的行为,不测实现细节;
2)不要改被测的代码;
3)每个新用例都要验证:临时把它要保护的那行代码改坏,确认这个用例会失败,再恢复;
4)最后重跑覆盖率,告诉我前后的变化;
5)哪些行为是碰巧如此、不该写进断言的,单独告诉我。

如果第二步里的几条规矩你每次都要说,把它们写成只对测试文件生效的规则,做法见CLAUDE.md 怎么写里“只对一部分文件生效的规则”一节。

这一趟花了多少

/cost 显示 0.95 美元,模型工作 4 分 4 秒,是这个系列到目前为止最贵的一次。原因是验证:6 次改坏、6 次重跑测试,再加上覆盖率的重跑,每一步都要带着越来越长的对话。这是 Claude Code 按官方价格估算的数字,不是中转站实际扣的金额。

想省一点,可以只对重要的用例要求“改坏验证”,简单的用例跳过。

这次实测没有覆盖到的

  • 只在一个 JavaScript 项目、一种测试框架上试过。 其他语言和框架上,覆盖率工具和“改坏”的做法都要相应调整。
  • 补的是三处简单的缺口。 风险最高的那一项(并发和连接中断)没有尝试。
  • 没有涉及给新功能先写测试。 这篇只讲给已有代码补测试。
  • 只跑了一次。 它这次找到了报告之外的缺口、纠正了自己的误判,不代表每次都会。所以复核那一步不能省。

来源

通过 API 中转站使用这些模型?到 Veridrop 免费检测中转站给你的是不是它声称的模型,或者看看中转站排行榜前 100 名。

相关文章