บทเรียน 18 · Day 5 · Test Practice

TDD: Red, Green, Refactor

~15 นาที · ต้องมี: บทเรียน 17 · บทสุดท้ายของ Day 5

TDD คือการเขียนเทสก่อนเขียนโค้ด ฟังดูกลับหัวกลับหาง แต่ผลที่ได้ไม่ใช่แค่ "มีเทส" — มันคือการบังคับให้คุณตัดสินใจว่า ฟังก์ชันนี้ควรหน้าตายังไงก่อนจะลงมือเขียน และในยุคที่ AI generate โค้ดให้ได้ทั้งไฟล์ในสิบวินาที เทสกลายเป็นสิ่งเดียวที่ยืนยันได้ว่าโค้ดนั้นตรงกับที่ธุรกิจต้องการจริง

1 · วงจร Red → Green → Refactor

ปกติเราเขียนโค้ดก่อน แล้วค่อยลองดูว่าใช้ได้ไหม TDD สลับสองอย่างนี้: เขียน "ตัวตรวจ" ก่อน แล้วค่อยเขียนของที่จะให้มันตรวจ ตัวตรวจในที่นี้คือเทส — โค้ดสั้น ๆ ที่เรียกฟังก์ชันของเรา แล้วเช็คว่าผลตรงกับที่ควรจะเป็นไหม
(production code ในตารางข้างล่าง = “โค้ดจริงที่จะได้ใช้งาน” ตรงข้ามกับโค้ดเทส)

       ┌──────────────────────────────────────────┐
       │                                          ▼
   [Red]  ─────────▶  [Green]  ─────────▶  [Refactor]
  เขียนเทสให้พัง      แก้ให้ผ่าน           ทำให้สวย
   (ไฟแดง)          (ไฟเขียว)        (โดยเทสยังเขียว)
ขั้นทำอะไรกฎเหล็ก
🔴 Redเขียนเทสที่อธิบายพฤติกรรมที่ต้องการ แล้วรันให้เห็นว่าพังห้ามเขียน production code ก่อนเห็นไฟแดง
🟢 Greenเขียนโค้ดน้อยที่สุดที่ทำให้ผ่านห้ามเขียนเกินความจำเป็น — สวยทีหลังได้
🔵 Refactorจัดโค้ดให้อ่านง่าย ลดซ้ำซ้อนเทสต้องเขียวตลอดเวลา ไม่แก้เทส
🧳 เทียบให้เห็นภาพ — ลิสต์ของก่อนแพ็กกระเป๋าไปเที่ยว

ก่อนไปเที่ยว หลายคนจดลิสต์ของที่ต้องเอาไปก่อน แล้วค่อยแพ็ก
· จดลิสต์จากทริปที่จะไป (ไปทะเล = ต้องมีครีมกันแดด) = เขียนเทส
· หยิบกระเป๋าใบที่จะใช้มาวาง เปิดออกมาเห็นว่าว่าง แล้วไล่เช็คทีละข้อว่าขาดอะไร = ไฟแดง
· แพ็กจนครบทุกข้อในลิสต์ ไม่ต้องเกิน = ไฟเขียว
· จัดของในกระเป๋าใหม่ให้เป็นระเบียบ โดยของยังครบเท่าเดิม = Refactor

ลองคิดกลับกันจะเห็นว่าทำไมลำดับถึงสำคัญ: ถ้าแพ็กก่อนแล้วค่อยจดลิสต์ตามของในกระเป๋า ลิสต์จะตรงกับกระเป๋า 100% เสมอ ต่อให้ลืมพาสปอร์ตก็ยังตรง

ที่ไม่เหมือนกัน: สองข้อที่การเทียบนี้พาไปผิดได้
· ต้องมีกระเป๋าให้เปิดก่อน ถึงจะนับว่าได้เช็ค — ถ้ายังไม่มีกระเป๋าเลย นั่นยังไม่ใช่ไฟแดง แต่คือ build failed ที่หัวข้อ 2 กำลังจะพูดถึง
· ลิสต์เที่ยวจดรอบเดียวจบ แต่ TDD วนเป็นรอบ ๆ · ตอนเขียนเองให้วนทีละเคส (เขียนเทส 1 เคส → ทำให้ผ่าน → ขึ้นเคสถัดไป) เพราะเป้าหมายคือคุมการออกแบบ · แต่ตอนให้ AI เขียน implementation ให้ล็อกสเปกทั้งตารางก่อน (หัวข้อ 6) เพราะเป้าหมายเปลี่ยนเป็นคุมไม่ให้มันโกง
นึกของที่ลืมออกทีหลังไม่ผิด แค่ต้องกลับไปเริ่มที่ Red ใหม่ ไม่ใช่แอบยัดโค้ดเพิ่มเงียบ ๆ

🗣 ศัพท์ที่เพิ่งเรียนไป
TDD (Test-Driven Development)
วิธีทำงานที่ให้เทสมาก่อนโค้ด · ถามให้ตรงคำว่า “ทีมนี้เขียนแบบ TDD จริง ๆ หรือแค่มีเทส” — สองอย่างนี้ไม่เหมือนกัน
production code
โค้ดจริงที่จะได้ใช้งาน (ตรงข้ามกับ test code) · จะได้ยินบ่อยว่า “ยังไม่ต้องแตะ production code จนกว่าจะเห็นแดง”
refactor
จัดโค้ดใหม่ให้อ่านง่ายขึ้น โดยพฤติกรรมเหมือนเดิมเป๊ะ · บอกคนรีวิวว่า “PR นี้ refactor อย่างเดียว ไม่เปลี่ยนพฤติกรรม” เขาจะรู้ทันทีว่าต้องดูอะไร

2 · Red มีสองแบบ — และมีแบบเดียวที่ใช่

ผมทดลองให้ดูแล้วด้วย Division(a, b float64) — เริ่มจากเขียนเทสก่อน โดยที่ยังไม่มีฟังก์ชันเลย

// division_test.go — เขียนอันนี้ก่อน
func TestDivision(t *testing.T) {
    got, err := Division(10, 2)
    if err != nil {
        t.Fatalf("ไม่ควรมี error: %v", err)
    }
    if got != 5 {
        t.Errorf("Division(10,2) = %v; want 5", got)
    }
}

🚫 Red แบบที่ยังไม่ใช่

$ go test ./...
# tdd [tdd.test]
./division_test.go:6:14: undefined: Division
FAIL	tdd [build failed]

นี่คือ build error ไม่ใช่ test failure — แปลว่ายังไม่ได้ทดสอบอะไรเลย เทสของคุณยังไม่เคยได้รันด้วยซ้ำ

✅ Red ที่ต้องการ

// เขียนโครงเปล่า ๆ ก่อน
func Division(a, b float64) (float64, error) {
    return 0, nil
}
$ go test ./...
--- FAIL: TestDivision (0.00s)
    division_test.go:11: Division(10,2) = 0; want 5
FAIL

เทสรันแล้วและพังด้วยเหตุผลที่ถูกต้อง — นี่คือหลักฐานว่าเทสของคุณทำงานจริง

ทำไมต้องเห็นไฟแดงก่อน

เพื่อพิสูจน์ว่าเทสจับได้จริง — เทสที่ไม่เคยแดงเลย อาจเป็นเทสที่ผ่านตลอดไม่ว่าโค้ดจะเป็นยังไง (ลืม assert, assert ผิดตัวแปร, เงื่อนไขกลับด้าน)
เขียนโค้ดก่อนแล้วค่อยเขียนเทส คุณจะไม่มีวันรู้ว่าเทสนั้นมีค่าจริงไหม

🔔 เทียบให้เห็นภาพ — เครื่องตรวจจับควันที่ติดเพดานไว้

เครื่องตรวจจับควันในบ้านเงียบมาสามปี แปลว่าอะไร? อาจแปลว่าบ้านปลอดภัยดี หรืออาจแปลว่าถ่านหมดไปตั้งแต่ปีที่แล้ว — มองจากข้างล่างเห็นเหมือนกันเป๊ะ
เขาถึงติดปุ่มกดทดสอบมาให้ กดแล้วต้องได้ยินเสียงร้อง นั่นคือหลักฐานว่ามันร้องเป็น

“เห็นไฟแดงก่อน” ก็คือการกดปุ่มทดสอบนั่นเอง — เทสที่ไม่เคยแดงเลย ก็เหมือนเครื่องที่ไม่เคยร้อง
ส่วน build failed ไม่ใช่ความเงียบ — มันคือกดปุ่มแล้วได้ยิน เสียงปี๊บสั้น ๆ ที่แปลว่าถ่านอ่อน แทนเสียงไซเรน · ดังเหมือนกัน แต่คนละเสียง คนละความหมาย
ได้ยินเสียงไม่พอ ต้องฟังด้วยว่ามันร้องเรื่องอะไร — ดูที่ output จริงข้างบนได้เลย: FAIL ที่ตามด้วย [build failed] คือเสียงเตือนถ่าน (เครื่องยังไม่ได้ดมควันสักครั้ง) ส่วน FAIL ที่ตามด้วย = 0; want 5 คือเสียงไซเรนของจริง
เพราะงั้นการเห็นคำว่า FAIL เฉย ๆ ยังไม่ใช่หลักฐานว่าได้ Red ที่ใช่

ที่ไม่เหมือนกัน: เครื่องตรวจจับควันกดปุ่มเดียวก็ทดสอบได้ทั้งเครื่อง (และคู่มือความปลอดภัยให้กดซ้ำเดือนละครั้ง ไม่ใช่ครั้งเดียวตอนติดตั้ง) แต่เทสแต่ละเคสตรวจได้แค่คำถามของตัวเอง — ไม่มีปุ่มเดียวที่ทดสอบได้ทุกอย่าง เพิ่มเคสใหม่ก็ต้องเห็นแดงของเคสนั้นใหม่ทุกครั้ง

🗣 ศัพท์ที่เพิ่งเรียนไป
build error / compile error
โค้ดยังแปลไม่ผ่าน จึงยังไม่ได้รันสักบรรทัด · เวลาขอความช่วยเหลือให้แยกให้ชัดว่า “มัน build ไม่ผ่าน” หรือ “มัน build ผ่านแต่เทสแดง” — คนละปัญหากันคนละวิธีแก้
test failure
เทสได้รันแล้ว แต่ผลไม่ตรงกับที่คาดไว้ · นี่คือ Red ที่ TDD ต้องการ
assert / assertion
บรรทัดในเทสที่ประกาศว่า “ผลต้องเป็นค่านี้” เช่น if got != 5 { ... } · ถ้าลืม assert เทสจะเขียวตลอดไม่ว่าโค้ดจะเป็นยังไง
ระวังตอนคุยกับคนอื่น: ใน testify (บทเรียน 17) คำนี้แคบกว่านั้น — assert = ไม่ผ่านแล้วเทสวิ่งต่อ ส่วน require = ไม่ผ่านแล้วหยุดทันที · ที่โค้ดหัวข้อ 6 ใช้ require.NoError กับ error แล้วใช้ assert.Equal กับค่า ก็เพราะถ้ามี error แล้วยังเช็คค่าต่อไปก็ไม่มีความหมาย
เทสที่ไม่ได้เทสอะไรเลย (vacuous / tautological test)
เทสที่ผ่านตลอดจนจับบั๊กไม่ได้ · ตอนรีวิวโค้ดให้ถามว่า “เทสตัวนี้เคยเห็นมันแดงหรือยัง” · ถาม AI ให้พูดว่า “ถ้าโค้ดผิด เทสชุดนี้จับได้ไหม” จะได้คำตอบที่มีประโยชน์กว่าถามว่า “เทสนี้ดีไหม”

3 · Green — เขียนน้อยที่สุด

func Division(a, b float64) (float64, error) {
    return a / b, nil          // พอแล้วสำหรับเทสที่มีตอนนี้
}
$ go test ./...
ok  	tdd	0.464s      ← 🟢

"แต่มันยังไม่กันหารด้วยศูนย์นี่" — ถูกต้อง และนั่นคือเคสถัดไปที่ต้องเขียนเทสก่อน ไม่ใช่สิ่งที่จะแอบใส่ตอนนี้
TDD บังคับให้ทุกบรรทัดของ production code มีเทสที่เรียกร้องมันอยู่ — ถ้าเขียนดักศูนย์ตอนนี้เลย บรรทัดนั้นจะไม่มีเทสไหนพิสูจน์ว่ามันทำงานถูก

รอบที่ 2 — Red อีกครั้ง

รอบนี้ยังไม่ต้องแปลงเป็นตาราง (เก็บไว้ทำตอน Refactor) — แค่เพิ่มเทสอีกฟังก์ชันสำหรับพฤติกรรมใหม่ และสังเกตว่าเทสนี้ยังไม่พูดถึง ErrDivideByZero เลย เพราะตัวแปรนั้นยังไม่ถูกประกาศ — ถ้าอ้างตอนนี้จะได้ build failed ซึ่งหัวข้อ 2 เพิ่งบอกว่ายังไม่ใช่ Red

// division_test.go — เพิ่มต่อท้าย
func TestDivision_ByZero(t *testing.T) {
    _, err := Division(10, 0)
    if err == nil {
        t.Errorf("Division(10,0) err = <nil>; want divide by zero")
    }
}
$ go test ./...
--- FAIL: TestDivision_ByZero (0.00s)
    division_test.go:18: Division(10,0) err = <nil>; want divide by zero
FAIL
FAIL	tdd	0.433s
FAIL      ← 🔴

Green อีกครั้ง

var ErrDivideByZero = errors.New("divide by zero")

func Division(a, b float64) (float64, error) {
    if b == 0 {
        return 0, ErrDivideByZero
    }
    return a / b, nil
}
$ go test ./...
ok  	tdd	0.464s      ← 🟢 ทั้งสองเคสผ่าน

4 · Refactor — ขั้นที่คนข้ามบ่อยที่สุด

ตอนนี้เทสเขียวแล้ว = คุณมีตาข่ายนิรภัย แก้โครงสร้างโค้ดได้อย่างมั่นใจ เพราะถ้าทำพัง เทสจะบอกทันที

กฎเดียวของขั้น Refactor

ห้ามเปลี่ยนพฤติกรรม และห้ามแก้เทสไปพร้อมกัน — ถ้าคุณแก้ทั้งโค้ดและเทสในรอบเดียว คุณจะไม่รู้ว่าเทสยังจับบั๊กได้อยู่ไหม
อยากเปลี่ยนพฤติกรรม = กลับไปขั้น Red เขียนเทสใหม่ก่อน

5 · TDD ได้อะไรมากกว่า "มีเทส"

สิ่งที่ได้ทำไม
ออกแบบ API ก่อนเขียน เทสคือผู้ใช้คนแรกของฟังก์ชันคุณ — ถ้าเขียนเทสแล้วรู้สึกอึดอัด แปลว่า signature (หน้าตาของฟังก์ชัน: ชื่อ + รับอะไรเข้า + คืนอะไรกลับ) ออกแบบไม่ดี รู้ตั้งแต่ยังไม่ลงทุนเขียน implementation (ไส้ในของฟังก์ชัน)
โค้ดเทสได้โดยธรรมชาติ คุณจะต้องใช้ DI — Dependency Injection คือการส่งของที่ฟังก์ชันต้องใช้เข้าไปทางพารามิเตอร์ แทนที่จะให้มันไปหยิบเอง (บทเรียน 17) — เพราะไม่งั้นเขียนเทสไม่ได้ตั้งแต่แรก
ไม่เขียนเกินที่ต้องการ ทุกบรรทัดมีเทสที่เรียกร้องมัน — ไม่มีโค้ดที่ "เผื่อไว้" แล้วไม่มีใครใช้
เทสที่พิสูจน์แล้วว่าจับได้ ทุกเทสเคยแดงมาก่อน → รู้แน่ว่ามันไม่ใช่เทสหลอก
รู้ว่า "เสร็จ" เมื่อไหร่ เทสเขียวครบ = เสร็จ · ไม่ต้องเดา ไม่ต้องเถียงกัน

6 · Test with AI workflow

ทุกวันนี้ AI generate โค้ดได้ทั้งไฟล์ในไม่กี่วินาที แต่โค้ดที่ได้อาจไม่ตรงกับสิ่งที่ธุรกิจต้องการ — AI เดาจากบริบทที่คุณให้ ถ้าบริบทกำกวม มันก็เดาไปทางที่ดูสมเหตุสมผลที่สุด ซึ่งอาจไม่ใช่สิ่งที่คุณหมายถึง

ปัญหา: AI เก่งเรื่อง "เขียนโค้ด" แต่ไม่รู้ "ธุรกิจต้องการอะไร"

  คุณ ──"เขียนฟังก์ชันคำนวณส่วนลด"──▶ AI ──▶ โค้ดที่ดูดี
                                              │
                                              ▼
                                   แล้วยังไงถึงจะรู้ว่าถูก?

ทางแก้: ให้เทสเป็นตัวกำหนดข้อตกลง แล้วปล่อย AI เขียน implementation

  คุณ ──▶ เขียนเทส (สเปกที่รันได้) ──▶ AI เขียนโค้ด ──▶ go test
                     ▲                                          │
                     └──────────── แดง = ยังไม่ตรง ◀────────────┘

ทำไมเทสเหมาะกับงานนี้เป็นพิเศษ

อย่าให้ AI เขียนเทสจากโค้ดที่มันเพิ่งเขียนเอง

ถ้าโค้ดผิด เทสที่เขียนตามโค้ดก็จะยืนยันความผิดนั้น ได้เทสเขียวสวยงามที่พิสูจน์ว่าบั๊กทำงานถูกต้องตามที่บั๊กเป็น
ลำดับที่ถูก: คุณกำหนดพฤติกรรม → เทสก่อน → โค้ดทีหลัง
ถ้าจะให้ AI ช่วยคิดเคส ให้มันเสนอรายการเคสมาก่อน (รวมถึงเคสขอบและเคสที่ควรพัง) แล้วคุณเลือกเอง — ต่างจากให้มันเขียนเทสจากโค้ดสำเร็จรูป

รูปแบบที่ใช้ได้จริง

1. คุณ:   เขียน table-driven test ที่ระบุพฤติกรรมทุกเคส (รวมเคสขอบ)
2. คุณ:   รันให้เห็นแดง — พิสูจน์ว่าเทสจับได้จริง
3. AI:    เขียน implementation ให้ผ่านเทสทั้งหมด
4. AI:    รัน go test เอง แก้เอง จนเขียว
5. คุณ:   อ่านโค้ดที่ได้ — ตรวจว่ามันไม่ได้ "โกง" เทส
6. คุณ:   เพิ่มเคสที่นึกออกเพิ่ม → กลับไปข้อ 2
// เทสที่คุณเขียน — มีเคสเดียว
func TestCalculateDiscount(t *testing.T) {
    got, err := CalculateDiscount(100, 10)
    require.NoError(t, err)
    assert.Equal(t, 90, got)
}

// implementation ที่ได้กลับมา
func CalculateDiscount(price, percent int) (int, error) {
    if price == 100 && percent == 10 {
        return 90, nil
    }
    return 0, nil
}

go test จะขึ้นเขียวหรือแดง — และคุณควรรู้สึกยังไงกับผลนั้น?

เขียว — เทสผ่านหมดทุกข้อ

โค้ดนี้ไม่ได้คำนวณส่วนลดเลย มันแค่จำคำตอบของเคสที่คุณเทสไว้ แต่เทสของคุณยืนยันไม่ได้ว่ามันผิด เพราะคุณถามคำถามเดียว

นี่คือสิ่งที่ AI ทำได้จริง ถ้าคุณสั่งแค่ "ทำให้เทสผ่าน" — และมันไม่ได้เจตนาโกง มันแค่ทำตามที่คุณขอเป๊ะ ๆ
ทางแก้ไม่ใช่การเลิกใช้ AI แต่คือการเขียนตารางเทสให้ครบ จนการโกงยากกว่าการเขียนสูตรจริง — เพิ่มเคส (100, 0), (250, 20), (100, 100) แล้วโค้ดข้างบนจะแดงทันที
นี่คือเหตุผลที่บทเรียน 16 ย้ำเรื่องเคสขอบตั้งแต่ก่อนจะมีเรื่อง AI

📝 เทียบให้เห็นภาพ — เด็กที่ท่องเฉลยข้อสอบเก่า

ครูออกข้อสอบเลขมาข้อเดียว: “ของราคา 100 ลด 10% เหลือเท่าไร” เฉลยคือ 90
นักเรียนที่คิดสูตรเป็นได้เต็ม · แต่นักเรียนที่ท่องมาว่า “เห็น 100 กับ 10 ให้ตอบ 90” ก็ได้เต็มเหมือนกัน
ข้อสอบใบนี้แยกสองคนนี้ไม่ออก จนกว่าครูจะเปลี่ยนโจทย์เป็น 250 ลด 20%

โค้ดที่ได้กลับมาข้างบนคือนักเรียนคนที่ท่องเฉลยเป๊ะ ๆ — ทางแก้คือออกข้อสอบให้ครบ ไม่ใช่เลิกให้ AI ทำข้อสอบ

ที่ไม่เหมือนกัน: นักเรียนรู้ตัวว่ากำลังเดา แต่ AI ไม่ได้เจตนาโกง มันทำตามโจทย์ที่ได้รับ (“ทำให้เทสผ่าน”) อย่างซื่อสัตย์ที่สุด · คนที่ต้องรับผิดชอบว่าโจทย์ครบหรือเปล่าคือคุณ ไม่ใช่มัน

🗣 ศัพท์ที่เพิ่งเรียนไป
hardcode
ฝังค่าคงที่ลงในโค้ดตรง ๆ แทนที่จะคำนวณออกมา · พูดว่า “ตรงนี้มัน hardcode ค่าไว้ ไม่ได้คำนวณจริง” จะสื่อกว่าบอกว่า “โค้ดนี้มันแปลก ๆ”
edge case (เคสขอบ)
เคสสุดขอบที่คนมักลืม เช่น 0, ค่าติดลบ, ค่าว่าง · สั่ง AI ว่า “ช่วยลิสต์ edge case ของฟังก์ชันนี้ ยังไม่ต้องเขียนโค้ด” จะได้ของที่มีประโยชน์กว่าปล่อยให้มันเขียนเทสเอง
executable specification
“สเปกที่รันได้” = เทสที่ทำหน้าที่เป็นสเปก คืออ่านแล้วรู้ว่าระบบต้องทำอะไร ไม่ใช่แค่เช็คว่าโค้ดทำอะไรอยู่ · ไม่ใช่ unit test ทุกตัวจะนับเป็นสเปก
เต็มรูปแบบของแนวคิดนี้คือ BDD / Specification by Example (Cucumber, Gherkin) ที่ฝั่งธุรกิจอ่านรู้เรื่องด้วย — พูดคำนี้กับ senior เขาอาจนึกถึงอันนั้น · แต่ table-driven test ที่ตั้งชื่อเคสดี ๆ ก็ทำหน้าที่นี้แทนได้
ประโยคที่ใช้สั่ง AI ได้จริง: “นี่คือเทสที่เป็นสเปก เขียน implementation ให้ผ่านทุกเคส รัน go test เองจนเขียว ห้ามแก้ไฟล์เทส”
coverage
สัดส่วนโค้ดที่เทสวิ่งผ่าน · ระวังกับดัก: มันบอกแค่ว่าเทสเคยวิ่งผ่านบรรทัดนั้น ไม่ได้บอกว่าตรวจผลถูกหรือเปล่า

ข้อ 5 ในลิสต์ข้างบนคือข้อที่ห้ามข้าม — เทสเขียวบอกแค่ว่า "ผ่านคำถามที่คุณถาม" ไม่ได้บอกว่า "โค้ดถูก" · คุณภาพของเทสคือเพดานของความมั่นใจ

7 · ทดสอบความจำ

1. ลำดับของวงจร TDD คืออะไร?

Red → Green → Refactor · หัวใจอยู่ที่ Red มาก่อน — ทำให้คุณออกแบบ API จากมุมของผู้ใช้ ก่อนจะติดกับ implementation ที่เขียนไปแล้ว และพิสูจน์ว่าเทสจับได้จริง

2. undefined: Division / FAIL [build failed] ถือเป็น Red ที่ใช้ได้ไหม?

build failed แปลว่าคอมไพล์ไม่ผ่าน — ยังไม่มีเทสตัวไหนได้รันสักตัว จึงพิสูจน์อะไรไม่ได้ Red ที่ต้องการคือเขียนโครงเปล่า ๆ ให้คอมไพล์ผ่าน แล้วเห็น --- FAIL: TestDivision ... = 0; want 5 ซึ่งพิสูจน์ว่าเทสทำงานและจับความผิดได้จริง

3. ขั้น Green บอกให้ "เขียนน้อยที่สุด" — ทำไม?

ถ้าเขียนดักเคสที่ยังไม่มีเทส บรรทัดนั้นจะไม่มีอะไรพิสูจน์ว่าถูก — และคุณจะไม่รู้ว่ามันทำงานจริงจนกว่าจะไป production
เคสถัดไป = เขียนเทสก่อน แล้วค่อยเพิ่มโค้ด ไม่ใช่แอบใส่ล่วงหน้า

4. ให้ AI เขียนเทสจากโค้ดที่มันเพิ่งเขียนเอง — ปัญหาคืออะไร?

เทสที่เขียนตามโค้ดจะบรรยายสิ่งที่โค้ดทำ ไม่ใช่สิ่งที่โค้ดควรทำ → ได้เทสเขียวสวยงามที่พิสูจน์ว่า บั๊กทำงานถูกต้องตามที่บั๊กเป็น
ลำดับที่ถูก: คนกำหนดพฤติกรรม → เทสก่อน → AI เขียนโค้ดให้ผ่าน

8 · ลงมือทำ

ภารกิจ: เขียน Division ด้วย TDD แบบเคร่งครัด (workshop จากสไลด์)

กติกา: ห้ามพิมพ์ production code ก่อนเห็นไฟแดงเด็ดขาด ทำทีละรอบ อย่าข้าม

  1. รอบ 1 — Red: เขียนเฉพาะ division_test.go ที่เทส Division(10, 2) == 5 → รัน → ได้ build failed ใช่ไหม? นั่นยังไม่ใช่ Red
  2. รอบ 1 — Red จริง: เพิ่ม func Division(a, b float64) (float64, error) { return 0, nil } → รัน → ต้องได้ = 0; want 5
  3. รอบ 1 — Green: เขียน return a / b, nil → เขียว
  4. รอบ 2 — เตรียมให้คอมไพล์ผ่านก่อน: ประกาศ var ErrDivideByZero = errors.New("divide by zero") ไว้ใน division.go แต่ยังไม่ต้องแตะตัวฟังก์ชัน — เหตุผลเดียวกับรอบ 1: ถ้าเทสอ้างของที่ยังไม่มี จะได้ build failed ซึ่งยังไม่ใช่ Red
  5. รอบ 2 — Red: เพิ่มเทสที่เรียก Division(10, 0) แล้วคาดว่า errors.Is(err, ErrDivideByZero) เป็นจริง → รัน → ต้องได้ err = <nil>; want divide by zero (แดงเพราะค่าไม่ตรง ไม่ใช่แดงเพราะคอมไพล์ไม่ผ่าน)
  6. รอบ 2 — Green: เพิ่มการดักศูนย์ → เขียว
  7. รอบ 3 — Red: คิดเคสเพิ่มเอง อย่างน้อย 3 เคส — แนะนำ: หารลงตัวไม่ลงตัว, ตัวเลขติดลบ, 0 / 5
    ระวัง: ผลลัพธ์เป็น float64 → ใช้ assert.InDelta ไม่ใช่ Equal (บทเรียน 17)
  8. Refactor: แปลงเทสทั้งหมดเป็น table-driven (บทเรียน 16) โดยที่เทสต้องเขียวตลอด — รัน go test หลังแก้ทุกครั้ง
  9. ท้าทาย 1 — พิสูจน์ว่าเทสไม่ใช่เทสหลอก: จงใจทำให้ Division ผิด (เช่นเปลี่ยนเป็น a * b) → เทสแดงกี่เคส? ถ้าแดงไม่หมดแปลว่ายังขาดเคส
  10. ท้าทาย 2 — ลอง AI workflow จริง:
    1. เขียนเทสอย่างเดียวสำหรับฟังก์ชันใหม่ เช่น ParseDuration(s string) (time.Duration, error) ที่รับ "1h30m", "45s", "abc"
    2. รันให้เห็นแดง
    3. ส่งเทสให้ AI แล้วบอกว่า "เขียน implementation ให้ผ่านเทสนี้ รัน go test เองจนเขียว ห้ามแก้ไฟล์เทส"
    4. อ่านโค้ดที่ได้ — มันโกงเทสหรือเปล่า? มี if s == "1h30m" ไหม?
    5. ถ้าโกง → เพิ่มเคสจนโกงไม่ได้ แล้วทำซ้ำ
🎉 จบ Day 5 แล้ว

ตอนนี้คุณเขียนเทสได้ · อ่านผลเทสออก · ออกแบบโค้ดให้เทสได้ · และใช้เทสเป็นสเปกคุม AI ได้
ห้าบทของ Day 5 ต่อกันเป็นเส้นเดียว: กฎเกม (14) → เครื่องมือ (15) → สำนวน (16) → library (17) → วิธีทำงาน (18)

อ่านต่อจากต้นทาง

แหล่งหลักที่แนะนำ: Learn Go with Tests ของ Chris James — สอน Go ทั้งภาษาผ่าน TDD ตั้งแต่ hello world ถึง concurrency ทุกบทเป็นวงจร red-green-refactor จริง ๆ ฟรี อ่านออนไลน์ได้ และเป็นแหล่งที่ชุมชน Go แนะนำกันมากที่สุดสำหรับเรื่องนี้
ถ้าจะอ่านต่อจากบทนี้แค่ที่เดียว ให้อ่านที่นั่น

💬 ติดตรงไหนถามได้เลย — ท้าทายข้อ 2 (AI workflow) คือข้อที่ผมอยากเห็นผลมากที่สุด ถ้า AI โกงเทสได้ ส่งโค้ดที่มันเขียนมา จะได้ดูด้วยกันว่าตารางเทสขาดเคสไหน — เป็นทักษะที่ใช้ได้ตลอดชีวิตการทำงาน

อ้างอิง