บันทึกเกี่ยวกับ test coverage
สวัสดีครับ วันนี่มีมิตรสหายมาสอบถามเกี่ยวกับ test coverage ในภาษา Go เลยเอามาเขียนบล็อกเก็บไว้ด้วย ในภาษา Go เราสามารถรันเทสด้วยคำสั่งนี้พร้อมกับ option -cover เพื่อดู code coverage ได้ > go test ./calculator -cover ok coverage/calculator 0.359s coverage: 100.0% of statements และยังสามารถสร้างเป็น HTML report ได้ ด้วยคำสั่ง > go test ./... -covermode=atomic -coverprofile=coverage.out > go tool cover -html=coverage.out -o coverage.html เมือเปิดดูไฟล์ HTML ที่ได้จะสามารถดูได้ว่าบรรทัดไหนบ้างที่ถูกใช้งานจากการรัน unit test สำหรับผู้ที่ใช้ VSCode ก็สามารถเปิด code coverage ได้เช่นกัน ด้วยการกด cmd(ctrl) + shift + p แล้วเลือกคำสั่งนี้ อย่างไรก็ตาม แม้เราจะมี code coverage ที่ 100% แล้วก็ไม่ได้แปลว่า unit test นั้นครบทุก case แล้ว ตัวอย่างเช่น function นี้ func Divide(a, b int) (float64, error) { if b == 0 { return 0, fmt.Errorf("division by zero") } return float64(a) / float64(b), nil } กับ unit test func TestDivide(t *testing.T) { tests := []struct { name string a, b int expected float64 expectError bool }{ {"valid division", 6, 2, 3.0, false}, {"division by zero", 6, 0, 0, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := Divide(tt.a, tt.b) if tt.expectError { if err == nil { t.Error("Divide() expected error but got none") } return } if err != nil { t.Errorf("Divide() unexpected error: %v", err) return } if result != tt.expected { t.Errorf("Divide(%d, %d) = %f; want %f", tt.a, tt.b, result, tt.expected) } }) } } เมื่อเรา run test ก็จะได้ 100% แน่นอนเพราะว่า test ของเราวิ่งเข้าครบทุกเงื่อนไข แต่ test coverage ไม่สามารถเตือนเราได้ว่า input เป็นเลขลบได้นะ! ดังนั้น เราควรจะเพิ่ม case เลขติดลบเข้าไปด้วย func TestDivide(t *testing.T) { tests := []struct { name string a, b int expected float64 expectError bool }{ {"valid division", 6, 2, 3.0, false}, {"division by zero", 6, 0, 0, true}, {"negative numbers", -6, 2, -3.0, false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := Divide(tt.a, tt.b) if tt.expectError { if err == nil { t.Error("Divide() expected error but got none") } return } if err != nil { t.Errorf("Divide() unexpected error: %v", err) return } if result != tt.expected { t.Errorf("Divide(%d, %d) = %f; want %f", tt.a, tt.b, result, tt.expected) } }) } } เพราะฉะนั้น Code coverage ไม่เท่ากับ Test case coverage นะครับ เพื่อป้องกันปัญหานี้การช่วยกันออกแบบ test case จึ่งมีความสำคัญ ลองทำ peer review โดยให้เพื่อนในทีม หรือ QA หรือ Product มาช่วยให้คำแนะนำก็ได้ แน่นอนว่าไม่ได้ให้มารีวิวโค้ด แต่ให้มาช่วยดู data ที่จะ test นั้นถูกต้อง เหมาะสม หรือไม่ หรือสมัยนี้สามารถใช้ AI เข้ามาช่วยวิเคราะห์ให้ได้เลยว่า test นั้นครบทุก edge case แล้วหรือยัง จะได้ออกมาเพียบ // Basic cases {"positive numbers", 6, 2, 3.0, false}, {"negative numbers", -6, 2, -3.0, false}, {"both negative", -6, -2, 3.0, false}, {"numerator negative", -6, 2, -3.0, false}, {"denominator negative", 6, -2, -3.0, false}, // Zero cases {"division by zero", 6, 0, 0, true}, {"zero numerator", 0, 5, 0.0, false}, {"zero denominator", 5, 0, 0, true}, {"both zero", 0, 0, 0, true}, // One cases {"divide by one", 5, 1, 5.0, false}, {"one by number", 1, 5, 0.2, false}, {"one by one", 1, 1, 1.0, false}, // Large numbers {"large positive", 1000000, 1000, 1000.0, false}, {"large negative", -1000000, 1000, -1000.0, false}, // Decimal results {"decimal result", 5, 2, 2.5, false}, {"small decimal", 1, 3, 0.3333333333333333, false}, // Edge cases {"max int32", 2147483647, 1, 2147483647.0, false}, {"min int32", -2147483648, 1, -2147483648.0, false}, {"max by max", 2147483647, 2147483647, 1.0, false}, {"min by min", -2147483648, -2147483648, 1.0, false}, หรือจะไปให้ไกลกว่านั้น ก็ลองใช้เทคนิคอื่นๆพวก monkey test หรือ permutation test เข้ามาช่วยในการทำการทดสอบอีกระดับก็ได้ครับ ลองศึกษาดูจาก link ต่อไปนี้ก่อนก็ได้ https://www.geeksforgeeks.org/monkey-software-testing/ https://www.youtube.com/watch?v=8hQG7QlcLBk&ab_channel=GopherAcademy Code coverage เป็นเครื่องมือที่มีประโยชน์มากในการพัฒนา software น่าเสียดายมากนะครับถ้าเอามาใช้เป็นแค่ quality gate เฉยๆ แต่ระหว่างทางเราไม่ได้เใช้ประโยชน์อะไรจากมันเลย ล

สวัสดีครับ วันนี่มีมิตรสหายมาสอบถามเกี่ยวกับ test coverage ในภาษา Go เลยเอามาเขียนบล็อกเก็บไว้ด้วย
ในภาษา Go เราสามารถรันเทสด้วยคำสั่งนี้พร้อมกับ option -cover เพื่อดู code coverage ได้
> go test ./calculator -cover
ok coverage/calculator 0.359s coverage: 100.0% of statements
และยังสามารถสร้างเป็น HTML report ได้ ด้วยคำสั่ง
> go test ./... -covermode=atomic -coverprofile=coverage.out
> go tool cover -html=coverage.out -o coverage.html
เมือเปิดดูไฟล์ HTML ที่ได้จะสามารถดูได้ว่าบรรทัดไหนบ้างที่ถูกใช้งานจากการรัน unit test
สำหรับผู้ที่ใช้ VSCode ก็สามารถเปิด code coverage ได้เช่นกัน ด้วยการกด cmd(ctrl) + shift + p แล้วเลือกคำสั่งนี้
อย่างไรก็ตาม แม้เราจะมี code coverage ที่ 100% แล้วก็ไม่ได้แปลว่า unit test นั้นครบทุก case แล้ว
ตัวอย่างเช่น function นี้
func Divide(a, b int) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return float64(a) / float64(b), nil
}
กับ unit test
func TestDivide(t *testing.T) {
tests := []struct {
name string
a, b int
expected float64
expectError bool
}{
{"valid division", 6, 2, 3.0, false},
{"division by zero", 6, 0, 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := Divide(tt.a, tt.b)
if tt.expectError {
if err == nil {
t.Error("Divide() expected error but got none")
}
return
}
if err != nil {
t.Errorf("Divide() unexpected error: %v", err)
return
}
if result != tt.expected {
t.Errorf("Divide(%d, %d) = %f; want %f", tt.a, tt.b, result, tt.expected)
}
})
}
}
เมื่อเรา run test ก็จะได้ 100% แน่นอนเพราะว่า test ของเราวิ่งเข้าครบทุกเงื่อนไข
แต่ test coverage ไม่สามารถเตือนเราได้ว่า input เป็นเลขลบได้นะ! ดังนั้น เราควรจะเพิ่ม case เลขติดลบเข้าไปด้วย
func TestDivide(t *testing.T) {
tests := []struct {
name string
a, b int
expected float64
expectError bool
}{
{"valid division", 6, 2, 3.0, false},
{"division by zero", 6, 0, 0, true},
{"negative numbers", -6, 2, -3.0, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := Divide(tt.a, tt.b)
if tt.expectError {
if err == nil {
t.Error("Divide() expected error but got none")
}
return
}
if err != nil {
t.Errorf("Divide() unexpected error: %v", err)
return
}
if result != tt.expected {
t.Errorf("Divide(%d, %d) = %f; want %f", tt.a, tt.b, result, tt.expected)
}
})
}
}
เพราะฉะนั้น Code coverage ไม่เท่ากับ Test case coverage นะครับ
เพื่อป้องกันปัญหานี้การช่วยกันออกแบบ test case จึ่งมีความสำคัญ ลองทำ peer review โดยให้เพื่อนในทีม หรือ QA หรือ Product มาช่วยให้คำแนะนำก็ได้ แน่นอนว่าไม่ได้ให้มารีวิวโค้ด แต่ให้มาช่วยดู data ที่จะ test นั้นถูกต้อง เหมาะสม หรือไม่
หรือสมัยนี้สามารถใช้ AI เข้ามาช่วยวิเคราะห์ให้ได้เลยว่า test นั้นครบทุก edge case แล้วหรือยัง จะได้ออกมาเพียบ
// Basic cases
{"positive numbers", 6, 2, 3.0, false},
{"negative numbers", -6, 2, -3.0, false},
{"both negative", -6, -2, 3.0, false},
{"numerator negative", -6, 2, -3.0, false},
{"denominator negative", 6, -2, -3.0, false},
// Zero cases
{"division by zero", 6, 0, 0, true},
{"zero numerator", 0, 5, 0.0, false},
{"zero denominator", 5, 0, 0, true},
{"both zero", 0, 0, 0, true},
// One cases
{"divide by one", 5, 1, 5.0, false},
{"one by number", 1, 5, 0.2, false},
{"one by one", 1, 1, 1.0, false},
// Large numbers
{"large positive", 1000000, 1000, 1000.0, false},
{"large negative", -1000000, 1000, -1000.0, false},
// Decimal results
{"decimal result", 5, 2, 2.5, false},
{"small decimal", 1, 3, 0.3333333333333333, false},
// Edge cases
{"max int32", 2147483647, 1, 2147483647.0, false},
{"min int32", -2147483648, 1, -2147483648.0, false},
{"max by max", 2147483647, 2147483647, 1.0, false},
{"min by min", -2147483648, -2147483648, 1.0, false},
หรือจะไปให้ไกลกว่านั้น ก็ลองใช้เทคนิคอื่นๆพวก monkey test หรือ permutation test เข้ามาช่วยในการทำการทดสอบอีกระดับก็ได้ครับ ลองศึกษาดูจาก link ต่อไปนี้ก่อนก็ได้
https://www.geeksforgeeks.org/monkey-software-testing/
https://www.youtube.com/watch?v=8hQG7QlcLBk&ab_channel=GopherAcademy
Code coverage เป็นเครื่องมือที่มีประโยชน์มากในการพัฒนา software น่าเสียดายมากนะครับถ้าเอามาใช้เป็นแค่ quality gate เฉยๆ แต่ระหว่างทางเราไม่ได้เใช้ประโยชน์อะไรจากมันเลย
ลองเอามาใช้เป็นเครื่องมือทำให้การเขียน code ของเราสนุกมากขึ้นดูนะครับ