Skip to main content

Coverage ที่มีความหมาย

ทำไม 100% Coverage ไม่ได้แปลว่าไม่มีบั๊ก​

Code coverage วัดแค่ว่า "บรรทัด/branch นี้ถูกรันผ่านหรือยัง" — มันไม่ได้วัดว่า assertion ที่เช็คผลลัพธ์นั้นถูกต้องและครบถ้วนแค่ไหน โค้ดที่ coverage 100% แต่ assertion อ่อนแอมาก (หรือ ไม่มีเลย) ก็ยังมีบั๊กซ่อนอยู่ได้เต็มไปหมด

// coverage 100% แต่ไม่พิสูจน์อะไรเลย
func TestDivide(t *testing.T) {
result, _ := Divide(10, 2)
_ = result // ไม่มี assertion ใดๆ! แค่เรียกฟังก์ชันให้ coverage วิ่งผ่าน
}
🧠 จำง่ายๆ

"Coverage บอกว่า 'ไปถึงจุดนั้นหรือยัง' ไม่ได้บอกว่า 'ไปถึงแล้วเช็คถูกไหม'" — เหมือนตรวจสอบว่า รถวิ่งผ่านทุกถนนในเมืองแล้วหรือยัง (coverage) แต่ไม่ได้เช็คว่าขับถูกเลนไหม ทำผิดกฎจราจรไหม ระหว่างทาง (assertion quality) วิ่งผ่านครบทุกถนนไม่ได้แปลว่าขับดี

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

มีแนวคิดที่แก้ปัญหานี้โดยตรงเรียกว่า Mutation Testing — แทนที่จะถามว่า "เทสต์รันผ่านโค้ด กี่ % " มันถามว่า "ถ้าแอบเปลี่ยนโค้ด (mutate) ให้ผิดนิดหน่อย เทสต์จะจับได้ไหม" เครื่องมือ (เช่น Stryker, PITest, go-mutesting) จะสร้าง "mutant" หลายพันตัวอัตโนมัติ (เช่น เปลี่ยน > เป็น >=, เปลี่ยน + เป็น -, เปลี่ยนค่า constant) แล้วรัน test suite ซ้ำกับแต่ละ mutant ถ้า test suite ยังผ่านอยู่ทั้งที่โค้ดถูกเปลี่ยนไปแล้ว (mutant "รอดชีวิต") แปลว่าเทสต์ชุดนั้น ไม่ได้ตรวจจับ behavior จริงๆ (coverage สูงแต่ "mutation score" ต่ำ) — mutation testing ให้ สัญญาณคุณภาพเทสต์ที่แม่นยำกว่า code coverage มาก แต่ก็ใช้เวลารันนานกว่ามาก (ต้องรัน test suite ซ้ำเป็นพันรอบ) ทำให้นิยมรันเฉพาะใน CI รอบพิเศษ ไม่ใช่ทุก commit เหมือน coverage ปกติ

Coverage ประเภทต่างๆ ที่ควรรู้จัก​

ประเภทวัดอะไรความเข้มงวด
Line coverageบรรทัดไหนถูกรันบ้างต่ำสุด (เจอง่ายสุดว่า 100% แต่ไม่มีความหมาย)
Branch coverageทุก if/else/switch ถูกรันครบทุกทางไหมปานกลาง
Path coverageทุก "เส้นทาง" การรันที่เป็นไปได้ทั้งหมดถูกรันไหมสูงสุด (แต่โตแบบ exponential ตามจำนวน branch — ทำครบ 100% แทบเป็นไปไม่ได้ในระบบจริง)
🧠 จำง่ายๆ

"Line = ไปถึงบ้านหรือยัง, Branch = เลี้ยวถูกทุกแยกไหม, Path = เดินครบทุกเส้นทางที่เป็นไปได้ ไหม" — ยิ่งละเอียดยิ่งมั่นใจ แต่ยิ่งวัดยากและมีจำนวนเพิ่มแบบทวีคูณตามความซับซ้อนของโค้ด

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

Branch coverage 100% ไม่ได้แปลว่า path coverage 100% ด้วย — ถ้าฟังก์ชันมี if 2 ตัวซ้อนกัน (4 path ที่เป็นไปได้: true-true, true-false, false-true, false-false) การมี 2 เทสต์ที่ครอบ "ทุก branch อย่างน้อยครั้งหนึ่ง" (เช่น เทส true-true กับ false-false) ก็ได้ branch coverage 100% แล้ว โดยไม่เคยเทส path true-false หรือ false-true เลย ซึ่งอาจเป็น path ที่มีบั๊กซ่อน อยู่พอดี — นี่คือเหตุผลที่ branch coverage 100% (ซึ่งเป็นมาตรฐานสูงที่สุดที่เครื่องมือ coverage ทั่วไปวัดได้ในทางปฏิบัติ) ยังไม่ใช่การรับประกันว่าไม่มีบั๊ก และทำไมทีมที่ serious เรื่อง quality จริงๆ จึงหันไปพึ่ง mutation testing หรือ property-based testing เสริมแทนที่จะพอใจแค่ branch coverage สูงๆ

Coverage Threshold ควรตั้งเท่าไหร่​

ไม่มีตัวเลขสากลที่ "ถูกต้อง" — แต่หลักปฏิบัติทั่วไปคือ ใช้ coverage เป็นสัญญาณเตือน (ว่า ส่วนไหนไม่มีเทสต์เลย) ไม่ใช่เป้าหมายปลายทาง (ว่าต้องไปให้ถึงตัวเลขให้ได้ไม่ว่าจะต้องเขียน เทสต์ไร้สาระแค่ไหนก็ตาม)

🧠 จำง่ายๆ

"Coverage = เครื่องวัดไข้ ไม่ใช่เป้าหมายการรักษา" — ไข้ลดไม่ได้แปลว่าหายป่วยเสมอไป (อาจกินยาลด ไข้อย่างเดียว) coverage สูงไม่ได้แปลว่าคุณภาพดีเสมอไป (อาจเขียนเทสต์ไร้ assertion แค่ไล่ตัวเลข) ใช้เป็นตัวช่วยสังเกต ไม่ใช่ KPI สูงสุดที่ไล่ตามอย่างเดียว

🎯 Beyond: จุดที่ชาวบ้านไม่ค่อยรู้

การตั้ง coverage threshold แบบ บังคับเป็นเกณฑ์ผ่าน-ไม่ผ่านใน CI (เช่น "ต้อง ≥ 80% ไม่งั้น build fail") มีผลข้างเคียงที่ไม่ตั้งใจที่เรียกว่า "coverage gaming" — developer ภายใต้ pressure เรื่อง deadline จะหาทางทำให้ตัวเลขผ่านเกณฑ์โดยไม่สนคุณภาพจริง (เขียนเทสต์ที่ไม่มี assertion ที่มีความหมายแบบตัวอย่างข้างบน) threshold ที่เข้มงวดเกินไปจึงอาจทำร้ายคุณภาพเทสต์ โดยรวมแทนที่จะช่วย เพราะเปลี่ยน incentive จาก "เขียนเทสต์ที่ดี" เป็น "ทำตัวเลขให้ผ่าน" — ทีมที่เข้าใจเรื่องนี้มักใช้ coverage threshold แบบ "ห้ามลดลงจากเดิม" (ratchet) แทนที่จะ บังคับตัวเลขสัมบูรณ์ตายตัว เพื่อป้องกันการถดถอยโดยไม่สร้างแรงกดดันที่ผิดทิศทาง

English Summary

Code coverage only measures whether a line/branch executed, not whether the assertion checking its result is meaningful — 100% coverage with weak or missing assertions still hides bugs. Mutation testing addresses this directly by introducing small code mutations and checking whether the test suite catches them; a surviving mutant signals a test gap coverage alone can't reveal. Branch coverage 100% still doesn't guarantee path coverage — nested conditionals can hit every branch individually while skipping entire combination paths where bugs hide. Treat coverage as a diagnostic signal (where are the untested gaps), not a target to chase — rigid CI thresholds can incentivize "coverage gaming" (assertion-free tests written just to hit a number), which is why some teams prefer a ratcheting "never decrease" policy over a fixed absolute bar.