teach-me-ai / sources / SP-3
⟵ กลับหน้าแทร็กเสริม
SP-3 · Superpowers

วินัยที่ทำให้โค้ดเชื่อถือได้: TDD, Debugging, Verification

⏱️ ~22 นาที 🎯 เข้าใจ red/green/refactor, วิธี debug เป็นระบบ และยืนยันงานด้วยหลักฐาน 📦 ต้นทาง: skills/test-driven-development · systematic-debugging · verification-before-completion

บทนี้เจาะ 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 ดูมัน fail จริง GREEN เขียนโค้ด ขั้นต่ำให้ผ่าน REFACTOR จัดโค้ดให้สวย test ต้องเขียวตลอด → วนกลับไป RED เรื่อยๆ ทีละ behavior
วงจร TDD 3 จังหวะ ห้ามข้ามข้อใดข้อหนึ่ง โดยเฉพาะ "verify RED" ที่คนมักข้าม

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 — ลำดับก่อนพูดว่า "เสร็จ"

  1. ระบุ ว่าคำสั่งอะไรพิสูจน์ข้อเคลมนี้ได้ (เช่น npm test, go build)
  2. รัน คำสั่งนั้นแบบเต็ม สดใหม่ ไม่ใช้ผลลัพธ์เก่า
  3. อ่าน output ทั้งหมด เช็ค exit code นับจำนวนที่ fail
  4. ตรวจ ว่า output ยืนยันข้อเคลมจริงไหม — ถ้าไม่ ให้บอกสถานะจริงพร้อมหลักฐาน
  5. แล้วค่อย พูดข้อเคลม พร้อมแนบหลักฐาน
ข้อเคลมต้องมีหลักฐานแบบไหนใช้ไม่ได้ (ไม่พอ)
Tests passoutput คำสั่ง test: 0 failuresรันครั้งก่อน, "น่าจะผ่าน"
Build สำเร็จคำสั่ง build: exit code 0linter ผ่าน, 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 ปิดช่องว่างสุดท้าย: ห้ามพูดว่าเสร็จโดยไม่มีหลักฐานจากการรันจริง
🎯 แบบฝึกหัด
  1. เลือกฟังก์ชันเล็กๆ ในโปรเจกต์ NestJS หรือ Go ของคุณ (เช่น validate input, format string) ลองทำ TDD จริง: เขียน test ก่อน รันให้เห็น FAIL ที่มีเหตุผลชัดเจน แคปหน้าจอ/แปะ output มาด้วย แล้วค่อยเขียนโค้ดขั้นต่ำให้ผ่าน
  2. นึกถึงบั๊กล่าสุดที่คุณเจอ ลองเขียนตามกรอบ Phase 1-4 ของ systematic-debugging ว่า ถ้าย้อนกลับไปตอนนั้น คุณจะ "gather evidence" ที่ boundary ไหนก่อน เพื่อรู้ว่าปัญหาอยู่ component ไหน
  3. พิมพ์คุยกับ Claude Code ในโปรเจกต์จริงว่า "ช่วยแก้บั๊ก [ระบุปัญหาจริง] ให้หน่อย" แล้วสังเกตว่า agent เริ่มด้วยการ investigate ก่อนเสนอ fix ไหม และก่อนบอกว่า "เสร็จแล้ว" มีการรันคำสั่งพิสูจน์จริงหรือไม่ — ถ้าไม่มี ลองถามตรงๆ ว่า "รันเทสต์ให้ดูหน่อยว่าผ่านจริง"