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
补漏,不是返工。这个细节很重要——后面会回来讲。
四条硬理由:
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 真正的价值。Claude 覆盖"它能想到的"测试,但它想不到的测试不会主动补。这正是 human 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.rb 和 model_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 倍。)
如果"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 会让你不敢放手让 Claude 写测试;接受"够好"才能启动分工。
不是所有测试都适合完全丢给 Claude:
这些场景里人类主导,Claude 做辅助。
把这个分工落成可执行的 default:
程序员看测试的心理资源比看代码更少。测试重复、机械、耗神但必须。这些特征正好是 Claude 的强项——它不会烦、不会累、不会到第 50 个 it 就偷工减料。
你做的事不是"写测试",是"保证有测试覆盖到的业务规则不漏"。一个是实现,一个是判断——判断留给你,实现交给 Claude。
119 spec / 1562 行一次上线且半个多月不返工,不是因为我更会写测试,是因为我完全没写测试。我只是比 Claude 多做一件事:知道哪些业务规则该被保护。