วินัยที่ทำให้โค้ดเชื่อถือได้: TDD, Debugging, Verification
บทนี้เจาะ 3 skill ที่เป็น "วินัย" (discipline) ของ Superpowers — ไม่ใช่เทคนิคใหม่ แต่เป็นกฎเหล็กที่บังคับให้ agent (และตัวคุณเองตอนเขียนโค้ด) ทำงานอย่างมีหลักฐานรองรับทุกก้าว แทนที่จะเดา แล้วหวังว่าจะถูก สามตัวนี้เชื่อมกันเป็นห่วงโซ่เดียว: เขียน test ก่อนเขียนโค้ด (TDD) → เจอบั๊กแล้ว ต้องหา root cause ก่อนแก้ (systematic-debugging) → ก่อนบอกว่า "เสร็จแล้ว" ต้องรันจริงแล้วดูผลลัพธ์ก่อน (verification-before-completion)
เหตุผลที่สามตัวนี้ต้องเรียนพร้อมกัน: ถ้าคุณข้าม TDD → โค้ดที่ไม่มี test มายืนยัน จะกลายเป็นสิ่งที่ debug ยากตอนพัง (เพราะไม่รู้ว่า "ควรทำงานยังไง") ถ้า debug แบบสุ่มเดา → แก้บั๊กหนึ่งจุด บั๊กใหม่โผล่ที่อื่น ถ้าไม่ verify ก่อนเคลมว่าเสร็จ → ทั้งสองอย่างข้างบนก็ไม่มีความหมาย เพราะสุดท้าย agent (หรือคุณ) ก็จะพูดว่า "น่าจะโอเคแล้ว" ทั้งที่ไม่ได้รันจริง
1. Test-Driven Development — RED → GREEN → REFACTOR
หลักการจาก test-driven-development/SKILL.md ตรงไปตรงมามาก:
เขียน test ก่อน
แล้ว "ดูมันล้มเหลวจริง" (watch it fail) ก่อนเขียนโค้ด implement
Iron Law: NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST — ถ้าเขียนโค้ดก่อน test ต้องลบทิ้งแล้วเริ่มใหม่ ห้ามเก็บไว้ "อ้างอิง" ห้าม "ปรับ" มันระหว่างเขียน test เพราะนั่นคือการเขียน test-after แบบแอบแฝง
RED — เขียน test ให้ล้มเหลวก่อน (mandatory verify)
ต้องรันจริงแล้วดูว่า test fail ด้วยเหตุผลที่ถูกต้อง (เพราะ feature ยังไม่มี ไม่ใช่ typo) ถ้า test ผ่านทันที แปลว่าคุณกำลังทดสอบ behavior ที่มีอยู่แล้ว — ต้องแก้ test ใหม่
// ตัวอย่าง NestJS/Jest — retry logic ยังไม่มี ต้อง fail ก่อน
test('retries failed operations 3 times', async () => {
let attempts = 0;
const operation = () => {
attempts++;
if (attempts < 3) throw new Error('fail');
return 'success';
};
const result = await retryOperation(operation);
expect(result).toBe('success');
expect(attempts).toBe(3);
});
// $ npm test retry.test.ts
// FAIL: retryOperation is not defined ← นี่คือ "fail ถูกเหตุผล"
GREEN — เขียนโค้ดขั้นต่ำให้ผ่าน แล้ว verify อีกครั้ง
เขียนแค่พอให้ test เขียว ห้ามเผื่อฟีเจอร์ที่ test ยังไม่ขอ (YAGNI — You Aren't Gonna Need It)
async function retryOperation<T>(fn: () => Promise<T>): Promise<T> {
for (let i = 0; i < 3; i++) {
try {
return await fn();
} catch (e) {
if (i === 2) throw e;
}
}
throw new Error('unreachable');
}
// $ npm test retry.test.ts
// PASS — output สะอาด ไม่มี warning
ในสาย Go ก็หลักการเดียวกัน — เขียน _test.go ก่อน แล้วรัน go test ./...
ให้เห็น FAIL ที่มีเหตุผลชัดเจน (เช่น "function not implemented") ก่อนค่อยเขียนฟังก์ชันจริง
// retry_test.go — เขียนก่อน implementation
func TestRetryOperation(t *testing.T) {
attempts := 0
op := func() (string, error) {
attempts++
if attempts < 3 {
return "", errors.New("fail")
}
return "success", nil
}
result, err := RetryOperation(op)
if err != nil || result != "success" {
t.Fatalf("want success got %v %v", result, err)
}
}
// $ go test ./...
// FAIL: undefined: RetryOperation ← fail ถูกเหตุผล ค่อยไป GREEN
REFACTOR — จัดโค้ดให้สวยโดย test ยังเขียว
ทำได้เฉพาะหลังผ่าน GREEN แล้วเท่านั้น: ลบโค้ดซ้ำ ตั้งชื่อให้ดีขึ้น แยกฟังก์ชันย่อย แต่ ห้ามเพิ่ม behavior ใหม่ ระหว่าง refactor — ถ้าอยากเพิ่มฟีเจอร์ ให้กลับไป RED ใหม่
Red flag ที่พบบ่อยที่สุด: test ไม่ผ่าน แล้ว "แก้ test ให้ผ่าน" แทนที่จะแก้โค้ด — นี่คือการโกงตัวเอง เพราะ test มีไว้บอกว่าโค้ดควรทำอะไร ไม่ใช่โค้ดทำอะไรอยู่แล้ว กฎจาก skill ชัดเจนว่า "Test fails? Fix code, not test." ข้อยกเว้นเดียวคือคุณพบว่า test เขียนผิด (เช่น expect ค่าผิดจริงๆ) — ต้องอธิบายเหตุผลให้ชัดก่อนแก้
2. Systematic Debugging — 4 phases ก่อนจะแก้บั๊ก
จาก systematic-debugging/SKILL.md: กฎเหล็กคือ
"NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST"
การเดาสุ่มแก้ (guess-and-check) ดูเหมือนเร็วกว่า แต่จริงๆ ช้ากว่า — ตัวเลขจริงจาก skill:
systematic ใช้เวลา 15-30 นาที ได้ผลถูกครั้งแรก 95% ส่วนแบบสุ่มเดาใช้ 2-3 ชั่วโมง ถูกแค่ 40%
| Phase | ทำอะไร | เสร็จเมื่อ |
|---|---|---|
| 1. Root Cause | อ่าน error/stack trace ให้ครบ, reproduce ให้ได้แน่นอน, เช็ค diff ล่าสุดที่เปลี่ยน, ใส่ log ตรง boundary ของแต่ละ component (เช่น API → service → DB) | เข้าใจ "เกิดอะไรขึ้น" และ "ทำไม" จริงๆ |
| 2. Pattern Analysis | หาโค้ดที่ทำงานได้ในลักษณะคล้ายกัน เทียบกับที่พัง ระบุความต่างทุกจุด แม้จุดเล็กที่คิดว่า "คงไม่เกี่ยว" | รู้ว่าอะไรต่างกันระหว่าง working vs broken |
| 3. Hypothesis | ตั้งสมมติฐานเดียว ชัดเจน ("คิดว่า X คือ root cause เพราะ Y") แก้ทีละจุดเล็กที่สุดเพื่อทดสอบ | ยืนยัน/ปฏิเสธสมมติฐานได้ |
| 4. Implementation | เขียน failing test ที่ reproduce บั๊กก่อน (ใช้ TDD) แล้วแก้ทีเดียว ไม่พ่วง refactor อื่น | test ผ่าน, ไม่มี test อื่นพัง, ปัญหาหายจริง |
กฎย่อยที่สำคัญ: ถ้าลองแก้ไปแล้ว 3 ครั้งยังไม่หาย ให้หยุด อย่าลองแก้ครั้งที่ 4 แต่ให้กลับไปตั้งคำถามกับสถาปัตยกรรม (architecture) แทน — เพราะ pattern แบบนี้มักแปลว่า ปัญหาไม่ได้อยู่ที่ symptom แต่อยู่ที่การออกแบบเชิงโครงสร้าง
ผูกกับงานจริง: เวลา MongoDB query คืนค่าผิด หรือ PostgreSQL constraint fail แบบงงๆ
อย่ารีบ "ลองใส่ .lean() ดูสิ" หรือ "ลอง disable foreign key ดูก่อน"
— ให้ trace data flow ย้อนกลับจากจุดที่ error โผล่ ไปจนถึงจุดที่ค่าถูกสร้างขึ้นมาก่อน (root-cause tracing)
3. Verification Before Completion — evidence ก่อน claim เสมอ
จาก verification-before-completion/SKILL.md: "Claiming work is complete
without verification is dishonesty, not efficiency." กฎเหล็กคือ
ห้ามเคลมสถานะใดๆ (ผ่านแล้ว/เสร็จแล้ว/fix แล้ว) โดยไม่ได้รันคำสั่งตรวจสอบในข้อความนี้จริงๆ
Gate function — ลำดับก่อนพูดว่า "เสร็จ"
- ระบุ ว่าคำสั่งอะไรพิสูจน์ข้อเคลมนี้ได้ (เช่น
npm test,go build) - รัน คำสั่งนั้นแบบเต็ม สดใหม่ ไม่ใช้ผลลัพธ์เก่า
- อ่าน output ทั้งหมด เช็ค exit code นับจำนวนที่ fail
- ตรวจ ว่า output ยืนยันข้อเคลมจริงไหม — ถ้าไม่ ให้บอกสถานะจริงพร้อมหลักฐาน
- แล้วค่อย พูดข้อเคลม พร้อมแนบหลักฐาน
| ข้อเคลม | ต้องมีหลักฐานแบบไหน | ใช้ไม่ได้ (ไม่พอ) |
|---|---|---|
| Tests pass | output คำสั่ง test: 0 failures | รันครั้งก่อน, "น่าจะผ่าน" |
| Build สำเร็จ | คำสั่ง build: exit code 0 | linter ผ่าน, log ดูโอเค |
| บั๊กแก้แล้ว | test ของ symptom เดิม: ผ่าน | แก้โค้ดแล้ว สมมติว่าหาย |
| Agent ทำงานเสร็จ | diff จริงใน VCS แสดงการเปลี่ยนแปลง | agent รายงานว่า "success" |
สัญญาณเตือนตัวเอง (red flags) จาก skill: ถ้าคุณกำลังจะพิมพ์คำว่า "should work", "probably", "seems to", หรือรู้สึกอยากพูด "เสร็จแล้ว!" ทั้งที่ยังไม่ได้รันคำสั่งในข้อความนี้ — ให้หยุดแล้วไปรันก่อน นี่ไม่ใช่กฎที่ยืดหยุ่นได้ "แค่ครั้งนี้"
สั่งงานจริงกับ Claude Code
เมื่อ 3 skill นี้ auto-trigger คุณจะเห็น agent พูดประมาณนี้ระหว่างทำงาน:
// ตัวอย่างบทสนทนา
ผู้ใช้: ช่วยเพิ่ม validation อีเมลว่างใน submitForm หน่อย
Claude: ผมจะเขียน test ที่ fail ก่อน (TDD) แล้วรันให้เห็น RED ก่อนแก้โค้ดนะครับ
// ... เขียน test, รัน npm test, แปะ output ที่ FAIL จริง ...
เห็นแล้วว่า fail เพราะยังไม่มี validation ตอนนี้จะเขียนโค้ดขั้นต่ำให้ผ่าน
// ... เขียนโค้ด, รัน npm test อีกครั้ง, แปะ output PASS จริง ...
ทดสอบผ่านแล้ว (2/2 tests, exit 0) — เสร็จงานครับ
สังเกตว่า agent ไม่พูดว่า "น่าจะโอเค" หรือ "ทำเสร็จแล้ว" ก่อนแปะ output จริง —
นี่คือผลของ verification-before-completion ที่ทำงานร่วมกับ TDD loop
สรุปการเชื่อมโยง 3 skill
ทั้งสามอย่างนี้ป้องกันสถานการณ์ "งานดูเหมือนเสร็จ แต่จริงๆ พัง":
- TDD ทำให้ทุกฟีเจอร์มี test ที่เคยเห็น fail จริง ไม่ใช่แค่ test ที่เขียนตามหลัง
- Systematic debugging ทำให้บั๊กถูกแก้ที่ root cause ไม่ใช่แค่ patch อาการ
- Verification ปิดช่องว่างสุดท้าย: ห้ามพูดว่าเสร็จโดยไม่มีหลักฐานจากการรันจริง
- เลือกฟังก์ชันเล็กๆ ในโปรเจกต์ NestJS หรือ Go ของคุณ (เช่น validate input, format string) ลองทำ TDD จริง: เขียน test ก่อน รันให้เห็น FAIL ที่มีเหตุผลชัดเจน แคปหน้าจอ/แปะ output มาด้วย แล้วค่อยเขียนโค้ดขั้นต่ำให้ผ่าน
- นึกถึงบั๊กล่าสุดที่คุณเจอ ลองเขียนตามกรอบ Phase 1-4 ของ systematic-debugging ว่า ถ้าย้อนกลับไปตอนนั้น คุณจะ "gather evidence" ที่ boundary ไหนก่อน เพื่อรู้ว่าปัญหาอยู่ component ไหน
- พิมพ์คุยกับ Claude Code ในโปรเจกต์จริงว่า "ช่วยแก้บั๊ก [ระบุปัญหาจริง] ให้หน่อย" แล้วสังเกตว่า agent เริ่มด้วยการ investigate ก่อนเสนอ fix ไหม และก่อนบอกว่า "เสร็จแล้ว" มีการรันคำสั่งพิสูจน์จริงหรือไม่ — ถ้าไม่มี ลองถามตรงๆ ว่า "รันเทสต์ให้ดูหน่อยว่าผ่านจริง"