Free

ให้ Claude เขียน test ส่วนคุณแค่ review: 1,562 บรรทัดจากสนามจริง

เกือบทั้ง 1,562 บรรทัดของ test ของ TopicReview เขียนโดย Claude ผมแค่ review—กว่าสองสัปดาห์บน production, commit ที่ตามมาทุกตัวเพิ่ม test ไม่มีตัวไหนเขียนใหม่ บทความนี้ว่าด้วยทำไม test คือเป้าหมายมอบหมายที่ดีที่สุด, ใน review ดูอะไรและไม่ดูอะไร (รวมถึง edge case ที่ขาดจริงใน spec) และค่าเริ่มต้นของการแบ่งงานนี้


บทความที่แล้วจบด้วย "119 spec เขียวหมด" ของ TopicReview คำถามถัดไปที่ผู้อ่านควรถามจริง ๆ: test พวกนั้นใครเขียน?

คำตอบ: Claude เขียนเกือบทั้งหมด 1,562 บรรทัดของโค้ด test ผมแค่ review มากกว่าสองสัปดาห์บน production แล้ว และรูปแบบการดูแล 1,562 บรรทัดนี้คือ เพิ่ม test ใหม่เท่านั้น ไม่เคยเขียน test เก่าใหม่

บทความนี้ว่าด้วยทำไม test คือตัวเลือกดีที่สุดสำหรับมอบหมายให้ Claude, ดูอะไรและไม่ดูอะไรใน review และการแบ่งงานนี้ไปได้ไกลแค่ไหนในทางปฏิบัติ

วางตัวเลขออกมาก่อน

test ของ TopicReview อยู่ใน 7 ไฟล์:

spec/services/topic_review_service_spec.rb   760 บรรทัด (88 test)
spec/requests/topic_reviews_spec.rb          281 บรรทัด (32 test)
spec/requests/review_appeals_spec.rb         152 บรรทัด (16 test)
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 test)
spec/models/topic_review_spec.rb              62 บรรทัด
───────────────────────────────────────────
                                          1,562 บรรทัด

ครอบคลุม test สี่ประเภท: service (ตรรกะธุรกิจ), request (controller + integration), policy (การอนุญาต Pundit), job (งานตามกำหนดเวลา)

commit แรก d162f1e มี Co-Authored-By: Claude Sonnet 4.6 และลงจอดด้วย 1,100+ บรรทัดจากทั้งหมดในครั้งเดียว commit 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

อุดช่องโหว่ ไม่ใช่ทำใหม่ รายละเอียดนี้จะย้อนกลับมาสำคัญทีหลัง

ทำไม test คือตัวเลือกดีที่สุดสำหรับมอบหมาย

เหตุผลแข็ง 4 ข้อ:

1. input และ output ชัดเจน. test โดยเนื้อแท้คือ "ให้ state นี้ → คาดหวังพฤติกรรมนี้" นี่คือจุดแข็งที่สุดของ Claude: แปล spec เป็น assertion โค้ดธุรกิจบางครั้งต้องชั่งน้ำหนัก, test แทบไม่ต้อง

2. เชิงกลไก × ปริมาณสูง. describe .open! เพียงอันเดียวต้องครอบคลุม "มีลูกขุนที่เข้าข่าย / ไม่มี / ไม่มี topic / มี review ที่กำลัง active อยู่แล้ว"—สี่ context, แต่ละอัน 2–5 it คนเริ่มตัดมุมที่ context ที่สาม Claude เขียน it ลำดับที่ 88 ด้วยความใส่ใจเท่ากับลำดับที่ 1

3. ลูป feedback สั้นมาก. เขียน test, รัน rspec, วินาทีก็รู้ว่าผ่านหรือไม่ โค้ดธุรกิจต้องใช้งานจริงหลายวันปัญหาถึงจะโผล่ ลูปสั้น = ข้อผิดพลาดของ Claude ถูก rspec จับได้ทันที—คุณไม่ต้องเฝ้า

4. ขนานโดยธรรมชาติ. บล็อก it เป็นอิสระต่อกัน ไม่มี coupling ซ่อน ขยายได้ง่ายมาก การสร้าง test แยกกันหลายสิบตัวพร้อมกันคือจุดที่ Claude เก่ง

ใน review ดูอะไร ไม่ดูอะไร

นี่คือแกนของการแบ่งงานทั้งหมด

ไม่ดู:

  • ไวยากรณ์ RSpec ถูกหรือไม่ → Claude แทบไม่ผิด
  • คุณภาพ mock → ถ้าไม่ใช่ over-mock ชัด ๆ โดยปกติก็โอเค
  • ความสวยของ factory → ไม่สำคัญ รันได้ก็รันได้
  • ความสม่ำเสมอของสไตล์ → ถ้ามีที่ไม่ลงตัว สั่ง Claude แก้ทีเดียวก็จบ

ดู:

  • edge case ครอบคลุมจริงหรือไม่
  • ชื่อ test อธิบายพฤติกรรมที่คาดหวังจริงหรือเปล่า
  • มี test ที่ควรมีแต่ไม่ได้เขียนไหม

ข้อสุดท้ายคือคุณค่าแท้ของ review Claude ครอบคลุม test "ที่เขานึกออก" แต่ test ที่เขานึกไม่ออกจะไม่เขียนขึ้นเอง นี่คือจุดที่ review ของมนุษย์เข้ามา—เดินย้อนจากกฎธุรกิจไปหาความครอบคลุมที่ขาด

ตัวอย่างเฉพาะ: review จับอะไรได้จริง ๆ

เปิดส่วนต้นของ describe ".open!" ใน spec/services/topic_review_service_spec.rb:

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 ทีนี้—test ไหนยืนยันว่า "ผู้เขียน post จะไม่ถูกเลือกเป็นลูกขุน"?

ค้น service_spec.rb และ model_spec.rb: ไม่มี model_spec.rb แค่ทดสอบไม่กี่เคสของ scope pending_vote_by ไม่ได้ครอบคลุม eligible_jurors โดยตรง service_spec.rb มีแค่คอมเมนต์เดียว: # Jurors must NOT be the post author—อยู่ใน setup ไม่ใช่ assertion

นี่คือสิ่งที่ review จับได้: กฎการกันสามข้อ (ผู้เขียน / ผู้รายงาน / เคยโหวตใน initial แล้ว) ไม่มีข้อใดได้รับการปกป้องโดย test ถ้าใครสักคน refactor eligible_jurors ทีหลังแล้วพลั้งหลุด post.user_id จาก list กันออก test เดิมทุกตัวจะผ่าน—และ production จะปล่อยให้ผู้เขียนนั่งในคณะลูกขุนของตัวเองอย่างเงียบ ๆ

Claude ไม่ผิด—เขาทดสอบตามที่ขอ เพียงแต่ไม่ถามเองว่า "กฎแต่ละข้อใน 3 ข้อนี้ต้องการ test ปกป้องไหม?" คำถามนั้น—จากกฎย้อนไปหาความครอบคลุม—คืองานของ review

(พูดตรง ๆ: ผมเองก็พลาดอันนี้ใน review แรก ไปเห็นเอาตอน audit ครั้งที่สองระหว่างเขียนบทความนี้ ดังนั้น review ก็ไม่ใช่ one-shot เหมือนกัน—แต่ก็ยัง ดีกว่าไม่ review 10 เท่า)

commit ที่ตามมายืนยันว่าการแบ่งงานนี้ทำได้จริงในทางปฏิบัติ

ถ้า "Claude เขียน + มนุษย์ review" สมบูรณ์แบบ หลัง commit แรกจะไม่มี commit test ใหม่อีก สิ่งที่เกิดขึ้นจริงน่าสนใจกว่า—อุดช่องโหว่แต่ไม่เขียนใหม่:

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

อันแรกเป็น regression test หลังบั๊ก—e8cb2db Default to keep verdict when review expires with zero votes คือ fix, 00393fc คือ test ที่คู่กัน รูปแบบเดียวกันกับอันที่สอง ตามหลัง abaa22e Fix CloseTopicReviewJob failing due to reasoning validation on old votes

สอง commit นี้พิสูจน์สองเรื่องพร้อมกัน:

  • review ไม่จับครบ 100%—production จึงเปิดเผยบั๊กสองตัว
  • แต่สถาปัตยกรรม test ยังแข็งแรงพอ เพิ่ม test ได้เรื่อย ๆ ไม่ต้องปรับโครงสร้าง—commit จึงเป็น "Add test for..." ไม่ใช่ "Rewrite ... spec"

"ดีพอ + ปะแก้ต่อได้เรื่อย ๆ" เป็นเกณฑ์ที่สมจริงกว่า "สมบูรณ์แบบ" มาก การวิ่งตาม review ที่สมบูรณ์แบบคือสิ่งที่หยุดคุณจากการมอบหมาย test ให้ Claude การยอมรับ "ดีพอ" คือสิ่งที่ขับเคลื่อนการแบ่งงานนี้

test ที่ไม่ควรมอบหมายให้ Claude ทำคนเดียว

ไม่ใช่ทุก test เหมาะกับ handoff เต็ม:

  • E2E happy-path—ต้องการมุมมองผลิตภัณฑ์ Claude เขียนได้แต่มักครอบคลุมแค่ "ทางเทคนิคเดินจบได้" และพลาด "ที่ผู้ใช้ติดจริง"
  • test ความปลอดภัย—ต้องการความคิดแบบผู้โจมตี Claude อนุรักษ์นิยม พลาดพื้นผิวโจมตีที่ไม่เป็นมาตรฐาน (การฉีดคีย์เวิร์ด SQL, สตริงยาวเกินปกติ, unicode สำรอง)
  • ค่าฐานประสิทธิภาพ—ต้องใช้ตัวเลขจาก environment จริง Claude เดาค่า threshold
  • การปรับโครงสร้าง fixture / factory ขนาดใหญ่—นี่คือระดับสถาปัตยกรรม กลับไป plan mode ไม่ใช่สิ่งที่ review จับได้

ในกรณีเหล่านี้ มนุษย์เป็นผู้นำ Claude ช่วย

การตั้งค่าเริ่มต้น

แปลงการแบ่งงานนี้เป็น default ที่ทำได้:

  1. ก่อนฟีเจอร์เริ่ม ผมอธิบาย กฎธุรกิจ (ไม่ใช่ข้อตกลง RSpec)
  2. Claude เขียน implementation และ test
  3. รัน test ผ่าน = เดินต่อ ล้มเหลว = Claude แก้เอง
  4. ผม review:
    • ไม่ดู syntax / mock / factory
    • ดูความครอบคลุม: กฎธุรกิจ ทุกข้อได้รับการปกป้องโดย test อย่างน้อย 1 ตัวหรือไม่
    • สอบถาม edge case: "0 แถว / null / concurrency / ละเมิด authz"—ไล่ทีละแกน
    • อ่านชื่อ test—ถ้าจากชื่อเดาไม่ออกว่าทดสอบอะไร ให้ Claude ตั้งชื่อใหม่
  5. บั๊กที่พบใน production กลับมาเป็น regression test—เป็นการสึกหรอปกติของการแบ่งงาน ไม่ใช่ความล้มเหลว

ปิดท้าย

โปรแกรมเมอร์มีทรัพยากรทางใจสำหรับอ่าน test น้อยกว่าอ่านโค้ด test ซ้ำซาก, เชิงกลไก, เหนื่อยใจ แต่จำเป็น ทั้งหมดนั้นตรงกับ sweet spot ของ Claude—ไม่เบื่อ ไม่เหนื่อย ไม่ตัดมุมที่ it ลำดับ 50

งานของคุณไม่ใช่ "เขียน test"—แต่คือ "ทำให้กฎธุรกิจทุกข้อถูก test ครอบคลุม" ด้านหนึ่งคือ implementation อีกด้านคือการตัดสินใจ การตัดสินใจอยู่กับคุณ, implementation ไปหา Claude

119 spec / 1,562 บรรทัด ส่งขึ้นใน commit เดียวและอยู่บน production กว่าสองสัปดาห์โดยไม่ต้อง rework—ไม่ใช่เพราะผมเขียน test ดีกว่า แต่เพราะ ผมไม่ได้เขียนเลย ผมแค่ทำสิ่งเดียวที่ Claude ไม่ทำ: ตัดสินใจว่ากฎธุรกิจข้อไหนสมควรได้รับการปกป้อง