免費

讓 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 多做一件事:知道哪些業務規則該被保護。