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 — ขั้นที่คนข้ามบ่อยที่สุด
ตอนนี้เทสเขียวแล้ว = คุณมีตาข่ายนิรภัย แก้โครงสร้างโค้ดได้อย่างมั่นใจ เพราะถ้าทำพัง เทสจะบอกทันที
- ตั้งชื่อตัวแปรใหม่ให้สื่อความหมาย
- แยกฟังก์ชันที่ยาวเกินไป
- ยุบ
ifซ้อนเป็น guard clause (บทเรียน 04) - เปลี่ยน
float64เป็น custom type ให้สื่อความหมาย (บทเรียน 09)
ห้ามเปลี่ยนพฤติกรรม และห้ามแก้เทสไปพร้อมกัน —
ถ้าคุณแก้ทั้งโค้ดและเทสในรอบเดียว คุณจะไม่รู้ว่าเทสยังจับบั๊กได้อยู่ไหม
อยากเปลี่ยนพฤติกรรม = กลับไปขั้น 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 ตรวจงานตัวเองได้ — บอกให้รัน
go testแล้วแก้จนเขียว โดยไม่ต้องรอคุณ review ทุกรอบ - คุณยังคุมความหมาย — คุณเป็นคนตัดสินใจว่า "ส่วนลด 100% แล้วราคาควรเป็น 0 หรือควร error" ซึ่ง 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 — Red: เขียนเฉพาะ
division_test.goที่เทสDivision(10, 2) == 5→ รัน → ได้build failedใช่ไหม? นั่นยังไม่ใช่ Red - รอบ 1 — Red จริง: เพิ่ม
func Division(a, b float64) (float64, error) { return 0, nil }→ รัน → ต้องได้= 0; want 5 - รอบ 1 — Green: เขียน
return a / b, nil→ เขียว - รอบ 2 — เตรียมให้คอมไพล์ผ่านก่อน: ประกาศ
var ErrDivideByZero = errors.New("divide by zero")ไว้ในdivision.goแต่ยังไม่ต้องแตะตัวฟังก์ชัน — เหตุผลเดียวกับรอบ 1: ถ้าเทสอ้างของที่ยังไม่มี จะได้build failedซึ่งยังไม่ใช่ Red - รอบ 2 — Red: เพิ่มเทสที่เรียก
Division(10, 0)แล้วคาดว่าerrors.Is(err, ErrDivideByZero)เป็นจริง → รัน → ต้องได้err = <nil>; want divide by zero(แดงเพราะค่าไม่ตรง ไม่ใช่แดงเพราะคอมไพล์ไม่ผ่าน) - รอบ 2 — Green: เพิ่มการดักศูนย์ → เขียว
- รอบ 3 — Red: คิดเคสเพิ่มเอง อย่างน้อย 3 เคส —
แนะนำ: หารลงตัวไม่ลงตัว, ตัวเลขติดลบ,
0 / 5
ระวัง: ผลลัพธ์เป็นfloat64→ ใช้assert.InDeltaไม่ใช่Equal(บทเรียน 17) - Refactor: แปลงเทสทั้งหมดเป็น table-driven
(บทเรียน 16) โดยที่เทสต้องเขียวตลอด —
รัน
go testหลังแก้ทุกครั้ง - ท้าทาย 1 — พิสูจน์ว่าเทสไม่ใช่เทสหลอก:
จงใจทำให้
Divisionผิด (เช่นเปลี่ยนเป็นa * b) → เทสแดงกี่เคส? ถ้าแดงไม่หมดแปลว่ายังขาดเคส - ท้าทาย 2 — ลอง AI workflow จริง:
- เขียนเทสอย่างเดียวสำหรับฟังก์ชันใหม่
เช่น
ParseDuration(s string) (time.Duration, error)ที่รับ"1h30m","45s","abc" - รันให้เห็นแดง
- ส่งเทสให้ AI แล้วบอกว่า "เขียน implementation ให้ผ่านเทสนี้ รัน go test เองจนเขียว ห้ามแก้ไฟล์เทส"
- อ่านโค้ดที่ได้ — มันโกงเทสหรือเปล่า?
มี
if s == "1h30m"ไหม? - ถ้าโกง → เพิ่มเคสจนโกงไม่ได้ แล้วทำซ้ำ
- เขียนเทสอย่างเดียวสำหรับฟังก์ชันใหม่
เช่น
ตอนนี้คุณเขียนเทสได้ · อ่านผลเทสออก · ออกแบบโค้ดให้เทสได้ ·
และใช้เทสเป็นสเปกคุม 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 โกงเทสได้ ส่งโค้ดที่มันเขียนมา จะได้ดูด้วยกันว่าตารางเทสขาดเคสไหน — เป็นทักษะที่ใช้ได้ตลอดชีวิตการทำงาน
อ้างอิง
- Learn Go with Tests — เรียน Go ทั้งภาษาผ่าน TDD (ฟรี)
- Add a test — Go tutorial — ทางการ เริ่มต้นสั้น ๆ
- Go Wiki — Table Driven Tests
- pkg.go.dev — testing
- Go Code Review Comments — Useful Test Failures