Free

Để Claude viết test, bạn chỉ review: 1.562 dòng thực chiến

Gần như toàn bộ 1.562 dòng test của TopicReview do Claude viết. Tôi chỉ review — hơn hai tuần trên production, mọi commit sau đó đều thêm test, không có commit nào viết lại. Bài này nói về vì sao test là nhiệm vụ lý tưởng để ủy thác, review nên xem và bỏ qua cái gì (kèm một edge case thiếu thực sự trong spec), và cấu hình mặc định của sự phân công này.


Bài trước khép lại với "119 spec xanh hết" cho TopicReview. Câu hỏi tiếp theo mà độc giả thực sự nên hỏi: ai viết các test đó?

Đáp: Claude viết gần như toàn bộ 1.562 dòng code test. Tôi chỉ review. Đã hơn hai tuần trên production, và khuôn mẫu bảo trì cho 1.562 dòng ấy là chỉ thêm test mới, không bao giờ viết lại test cũ.

Bài này nói về vì sao test là việc tốt nhất để ủy thác cho Claude, review thì xem gì và bỏ qua gì, cùng mức độ mà sự phân công này thực sự đi được.

Trước hết, trải các con số ra

Test của TopicReview nằm trong 7 file:

spec/services/topic_review_service_spec.rb   760 dòng (88 test)
spec/requests/topic_reviews_spec.rb          281 dòng (32 test)
spec/requests/review_appeals_spec.rb         152 dòng (16 test)
spec/requests/review_votes_spec.rb           127 dòng
spec/policies/topic_review_policy_spec.rb    109 dòng
spec/jobs/close_topic_review_job_spec.rb      71 dòng (7 test)
spec/models/topic_review_spec.rb              62 dòng
───────────────────────────────────────────
                                          1.562 dòng

Bao trùm bốn loại test: service (logic nghiệp vụ), request (controller + integration), policy (phân quyền Pundit), job (tác vụ lịch).

Commit đầu d162f1e kèm Co-Authored-By: Claude Sonnet 4.6, đẩy hơn 1.100 dòng trong số đó trong một lần. Mọi commit spec sau đó đều là "Add test for..."—không một dòng refactor hay 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

Vá lỗ hổng, không làm lại. Chi tiết này quan trọng ở phần sau.

Vì sao test là việc nên giao cho Claude nhất

Bốn lý do cứng:

1. Đầu vào và đầu ra rõ ràng. Bản chất của test là "cho state này → mong đợi hành vi kia". Đây chính là mặt mạnh nhất của Claude: dịch spec thành assertion. Code nghiệp vụ đôi khi cần cân nhắc đánh đổi; test gần như không.

2. Máy móc × số lượng lớn. Một describe .open! phải phủ "có bồi thẩm đủ tư cách / không có / không có topic / đã có review đang hoạt động"—bốn context, mỗi cái 2–5 it. Người đến context thứ ba bắt đầu cắt gọn. Claude viết it thứ 88 với cùng độ cẩn trọng như cái thứ nhất.

3. Vòng phản hồi cực ngắn. Viết test, chạy rspec, vài giây là biết đậu hay rớt. Code nghiệp vụ mất vài ngày sử dụng thật mới lộ vấn đề. Vòng ngắn = lỗi của Claude bị rspec tóm tại chỗ—bạn không cần canh.

4. Song song tự nhiên. Các it độc lập, không có phụ thuộc ẩn, dễ mở rộng. Sinh hàng chục test độc lập cùng lúc đúng là sở trường của Claude.

Review thì xem gì, bỏ qua gì

Phần này là trục của cả sự phân công.

Bỏ qua:

  • Cú pháp RSpec có đúng không → Claude hầu như không sai
  • Chất lượng mock → trừ khi over-mock rõ rệt, còn lại ổn
  • Thẩm mỹ factory → không quan trọng, chạy là được
  • Sự đồng nhất phong cách → có vấn đề thì bảo Claude sửa đồng loạt

Xem:

  • Edge case có thực sự được phủ hết không
  • Tên test có mô tả đúng hành vi kỳ vọng không
  • Có test nào đáng có mà chưa viết không

Mục cuối là giá trị thật của review. Claude phủ được những test "nó nghĩ ra", nhưng những test nó không nghĩ ra sẽ không tự xuất hiện. Đó đúng là chỗ của review con người—truy ngược từ quy tắc nghiệp vụ về độ phủ đang thiếu.

Ví dụ cụ thể: review thực sự bắt được gì

Mở đầu describe ".open!" trong spec/services/topic_review_service_spec.rb:

describe ".open!" do
  context "when there are eligible jurors" do
    # trạng thái review đúng / post under_review / tạo assignments / báo author / báo jurors / không mở đôi
  end
  context "when there are no eligible jurors" do
    # review được tạo nhưng không tạo assignments
  end
  context "when post has no topic" do
    # trả về nil
  end
end

Trông có vẻ toàn diện. Nhưng quy tắc thực của eligible_jurors trong model loại trừ ba nhóm:

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

Giờ nhìn test—test nào khẳng định "tác giả của post không bao giờ được chọn làm bồi thẩm"?

Lục service_spec.rbmodel_spec.rb: không có. model_spec.rb chỉ test vài trường hợp của scope pending_vote_by; không phủ eligible_jurors trực tiếp. service_spec.rb chỉ có một dòng comment: # Jurors must NOT be the post author—là comment trong setup, không phải assertion.

Đây là điều review có thể bắt: ba quy tắc loại trừ (tác giả / người tố giác / đã-vote-initial), không một quy tắc nào được test bảo vệ. Sau này ai đó refactor eligible_jurors và lỡ tay đánh rơi post.user_id khỏi danh sách loại trừ, tất cả test hiện có vẫn pass—và production lặng lẽ cho tác giả ngồi trong bồi thẩm đoàn của chính họ.

Claude không sai—nó test đúng điều được yêu cầu. Nó chỉ không chủ động hỏi: "mỗi trong ba quy tắc này có cần được test bảo vệ không?" Câu hỏi đó—truy ngược từ quy tắc về độ phủ—chính là việc review cần làm.

(Nói thật: tôi cũng bỏ sót chỗ này ở lần review đầu. Chỉ thấy ra khi audit lần hai để viết bài này. Tức là review cũng không phải một phát ăn ngay—nhưng vẫn tốt hơn không review gấp 10 lần.)

Các commit sau xác nhận sự phân công này thực sự hoạt động

Nếu "Claude viết + người review" là phân công hoàn hảo, sẽ không có commit test mới sau commit đầu. Thực tế thú vị hơn—có vá lỗ hổng nhưng không viết lại:

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

Cái đầu là regression test hậu bug—e8cb2db Default to keep verdict when review expires with zero votes là fix, 00393fc là test đi kèm. Cái thứ hai cùng mô hình, đi sau abaa22e Fix CloseTopicReviewJob failing due to reasoning validation on old votes.

Hai commit này chứng minh hai điều cùng lúc:

  • Review không bắt hết 100% case—nên production lộ hai bug
  • Nhưng kiến trúc test đủ ổn, ta tiếp tục thêm test mà không cần tái cấu trúc—nên commit là "Add test for..." chứ không phải "Rewrite ... spec"

"Đủ tốt + có thể tiếp tục vá" là thước đo thực tế hơn "hoàn hảo" rất nhiều. Đuổi theo review hoàn hảo sẽ khiến bạn không dám giao test cho Claude. Chấp nhận 'đủ tốt' mới khởi động được sự phân công này.

Test không nên giao trọn cho Claude

Không phải mọi test đều hợp với handoff toàn phần:

  • E2E happy-path—cần lăng kính sản phẩm. Claude viết được nhưng dễ chỉ phủ "về mặt kỹ thuật chạy hết", bỏ qua "người dùng thực tế tắc ở đâu"
  • Test bảo mật—cần tư duy kẻ tấn công. Claude bảo thủ, dễ bỏ qua bề mặt tấn công phi chuẩn (chèn từ khóa SQL, chuỗi siêu dài, unicode thay thế)
  • Baseline hiệu năng—cần con số từ môi trường thực. Claude sẽ đoán ngưỡng
  • Đại tu fixture / factory phạm vi lớn—đây là thay đổi cấp kiến trúc; quay về plan mode, không phải cái review bắt được

Trong các trường hợp này, người dẫn và Claude phụ.

Cấu hình mặc định

Biến sự phân công thành default thực thi được:

  1. Trước khi tính năng bắt đầu, tôi giải thích quy tắc nghiệp vụ (không phải quy ước RSpec)
  2. Claude viết triển khai và test
  3. Chạy test. Pass = tiếp. Fail = Claude tự sửa
  4. Tôi review:
    • Không nhìn cú pháp / mock / factory
    • Nhìn độ phủ: mỗi quy tắc nghiệp vụ có ít nhất một test bảo vệ không
    • Tra vấn edge case: "0 dòng / null / concurrency / vi phạm authz"—từng trục một
    • Đọc tên test—nếu từ tên không đoán được nó test gì, bảo Claude đặt lại tên
  5. Bug phát hiện ở production quay về thành regression test—đó là hao mòn bình thường của sự phân công, không phải thất bại

Kết

Lập trình viên có ít năng lượng tâm trí để đọc test hơn đọc code. Test lặp lại, máy móc, mệt người, nhưng cần thiết. Tất cả đều trùng với vùng mạnh của Claude—không chán, không mệt, không cắt khúc ở it thứ 50.

Việc của bạn không phải là "viết test"—mà là "đảm bảo mọi quy tắc nghiệp vụ được test phủ". Một bên là triển khai, bên kia là phán đoán. Phán đoán ở lại với bạn; triển khai đi về Claude.

119 spec / 1.562 dòng ship trong một commit và sống qua hơn hai tuần không phải rework—không phải vì tôi viết test giỏi hơn, mà vì tôi không viết gì cả. Tôi chỉ làm một việc Claude không làm: quyết định quy tắc nghiệp vụ nào đáng được bảo vệ.