免费

让 Claude 写测试,你来 review——1562 行的分工实战

TopicReview 的 1562 行测试几乎全部是 Claude 写的,我只做 review——上线半个多月只加新测试、不改旧测试。这篇讲为什么测试是 Claude 最该干的活,review 该看什么不看什么(以及真实 spec 里一个被遗漏的边界),以及这种分工的默认配置。


上一篇结尾说 TopicReview 的"119 spec 全绿"。读者真正该问的下一个问题:这些测试谁写的?

答:Claude 写了几乎所有 1562 行测试代码,我只做 review。上线到现在半个多月,这 1562 行的后续维护是只加新测试不改旧测试

这篇讲为什么测试是 Claude 最该干的活,review 时该看什么不看什么,以及这个分工实际能走多远。

先把数字摊出来

TopicReview 的测试分在 7 个文件里:

spec/services/topic_review_service_spec.rb   760 行(88 个测试)
spec/requests/topic_reviews_spec.rb          281 行(32 个测试)
spec/requests/review_appeals_spec.rb         152 行(16 个测试)
spec/requests/review_votes_spec.rb           127 行
spec/policies/topic_review_policy_spec.rb    109 行
spec/jobs/close_topic_review_job_spec.rb      71 行(7 个测试)
spec/models/topic_review_spec.rb              62 行
───────────────────────────────────────────
                                          1,562 行

覆盖四种测试类型:service(业务逻辑)、request(controller + integration)、policy(Pundit 权限)、job(定时任务)。

初始 commit d162f1e 带 Co-Authored-By: Claude Sonnet 4.6,一次性写了其中 1100+ 行。之后的 spec 变动全是"Add test for..."——没有一条是 refactor 或 rewrite

00393fc Add test for finalize! with zero votes (expired review)
3f53304 Add test for finalize! with legacy votes missing reasoning
3b185da Update specs to use PROVISIONAL_PENALTY constant

补漏,不是返工。这个细节很重要——后面会回来讲。

为什么测试是 Claude 最该写的活

四条硬理由:

1. 输入输出明确。测试本质是"给定状态 → 期待行为"。这是 Claude 最拿手的翻译——把规范翻译成断言。代码逻辑有时候还要权衡、琢磨,测试没什么可琢磨的。

2. 机械 × 高数量。一个 describe .open! 下要覆盖"有陪审员 / 无陪审员 / 无 topic / 已有 active review"四种 context,每个 context 再铺 2-5 个 it block。人类写到第三种 context 会本能想偷懒省一个 case。Claude 不会偷懒——它写第 88 个 it 和第 1 个的认真程度完全一样。

3. 反馈闭环超短。测试写完立刻跑 rspec,通不通过马上知道。不像业务代码要等实际用几天才暴露问题。闭环短意味着 Claude 的错误会被 rspec 立刻拦下来,你不需要盯着看。

4. 天然并行it block 之间相互独立,没有隐藏依赖,可以大批量铺展。Claude 一次生成几十个独立测试正是它的强项。

Review 时看什么,不看什么

这部分是整个分工的关键。

不看

  • RSpec 语法对不对 → Claude 几乎不会错
  • mock 写得好不好 → 除非明显 over-mock,一般没问题
  • Factory 用得优雅不优雅 → 不重要,能跑就行
  • 测试风格统一不统一 → 存在问题时 Claude 一改全改,不用人力

  • 边界 case 是不是真的都覆盖了
  • 测试名字描述的行为是否真的是期望行为
  • 有没有应该存在但没写的测试

最后一条是 review 真正的价值。Claude 覆盖"它能想到的"测试,但它想不到的测试不会主动补。这正是 human review 的定位——从业务规则反查缺失

一个具体例子:review 实际在发现什么

打开 spec/services/topic_review_service_spec.rb 开头的 describe ".open!"

describe ".open!" do
  context "when there are eligible jurors" do
    # review 状态正确 / post under_review / 创建 assignments / 通知 author / 通知 jurors / 不重复开
  end
  context "when there are no eligible jurors" do
    # 创建 review 但不创建 assignments
  end
  context "when post has no topic" do
    # 返回 nil
  end
end

看起来很全。但 eligible_jurors 在 model 里的真实规则是排除三类人:

def eligible_jurors
  excluded_ids = [ post.user_id ] + post.reports.pluck(:user_id) + review_votes.where(stage: :initial).pluck(:user_id)
  User.jurors_and_judges.where.not(id: excluded_ids.uniq)
end

现在看测试——哪个 test 断言了"原作者不会被选进陪审员"?

翻遍 service_spec.rbmodel_spec.rb,没有。model_spec.rb 只测了 pending_vote_by scope 的几种情况,没测 eligible_jurors 本身。service_spec.rb 里只有一行注释:# Jurors must NOT be the post author——是 setup 的注释,不是断言。

这就是 review 能发现的缺失:三条排除规则(原作者 / 举报人 / 已投过的),没有一条被测试保护。如果以后改 eligible_jurors 的实现——比如有人重构时不小心把 post.user_id 漏掉——所有现有测试都会通过,但生产会让原作者进自己的陪审团。

Claude 没错,它测了它被要求测的东西。但它没主动问"这三条规则分别需要测试保护吗"。这种提问——从规则反查测试覆盖——就是 review 要干的事。

(实话说,我 review 的时候也漏了这个。是写这篇文章二次 audit 时才看到的。这说明 review 也不是一次到位——但比不 review 好 10 倍。)

后续 commit 印证了这个分工的现实性

如果"Claude 写 + human review"是完美分工,初始 commit 之后就不应该有新测试 commit 了。实际情况更有意思——有补漏但没有重写

00393fc Add test for finalize! with zero votes (expired review)
3f53304 Add test for finalize! with legacy votes missing reasoning

第一条是发现 bug 后补的——e8cb2db Default to keep verdict when review expires with zero votes 是 fix,00393fc 是配套的 regression test。第二条同理跟在 abaa22e Fix CloseTopicReviewJob failing due to reasoning validation on old votes 后面。

这两条 commit 证明两件事:

  • review 没覆盖 100% 的 case——所以生产环境暴露了两个 bug
  • 但测试架构足够稳,能持续加新测试,不用重构——所以补漏是"Add test for..."而不是"Rewrite ... spec"

"够好 + 能持续打补丁"这个标准比"完美"现实得多。追求完美 review 会让你不敢放手让 Claude 写测试;接受"够好"才能启动分工

不该让 Claude 独立写的测试

不是所有测试都适合完全丢给 Claude:

  • E2E 快乐路径——需要产品视角,Claude 能写但容易只覆盖"技术上能跑完",漏掉"用户真实会卡在哪"
  • 安全测试——需要攻击者思维,Claude 保守,容易漏掉非标准攻击面(SQL 关键字注入、超长字符串、替代 unicode 等)
  • 性能基线——需要真实部署环境的数字,Claude 容易瞎编阈值
  • 影响范围大的 fixture / factory 改造——这属于架构级改动,应该回到 plan mode,不是 review 出来的

这些场景里人类主导,Claude 做辅助。

默认配置

把这个分工落成可执行的 default:

  1. 功能开始前,我把业务规则讲清楚(不是 RSpec 规则)
  2. Claude 写实现 + 写测试
  3. 跑测试,通过 = 继续,失败 = Claude 自己修
  4. 我 review:
    • 不看语法/mock/factory
    • 看覆盖面:每条业务规则是不是至少有一个测试保护它
    • 追问 edge case:"0 条/空值/并发/越权"几个维度一个一个问
    • 看测试名——如果看名字猜不到测了什么,让 Claude 重命名
  5. 生产发现的 bug 回来补 regression test——这是分工的正常损耗,不是失败

结尾

程序员看测试的心理资源比看代码更少。测试重复、机械、耗神但必须。这些特征正好是 Claude 的强项——它不会烦、不会累、不会到第 50 个 it 就偷工减料。

你做的事不是"写测试",是"保证有测试覆盖到的业务规则不漏"。一个是实现,一个是判断——判断留给你,实现交给 Claude。

119 spec / 1562 行一次上线且半个多月不返工,不是因为我更会写测试,是因为我完全没写测试。我只是比 Claude 多做一件事:知道哪些业务规则该被保护。