วันนี้จะมาลองใช้ Go เพื่อทำ unit testing อย่างง่าย เพื่อทำความเข้าใจสำหรับมือใหม่ให้เห็นภาพไปพร้อมกัน ด้วยความที่ Go มี function ที่อำนวยความสะดวกในการทำ testing รวมถึงมีคำสั่งในการ run test ซึ่งเรียกใช้ได้เลยโดยไม่ต้องลง plug in เพิ่มเติม จึงเหมาะมากที่จะใช้เป็นตัวอย่างสำหรับ TDD
Test Driven Development (TDD)
TDD มีหลักการอย่างง่าย คือ ให้สร้าง test สำหรับ function ที่ยังไม่ implement ขึ้นมาก่อน จากนั้นให้ค่อยๆ เริ่มเขียนโค้ดเพิ่มเติมเพื่อให้ได้ผลลัพธ์ตามที่เราคาดหวัง โดยมากแล้วมักมีวงจรดังนี้
- Red: ประกาศตัวแปร, function ที่ใช้ในการทดสอบ (testcase) ซึ่งผลลัพธ์ที่คาดหวังคือ fail
- Green: เขียนโค้ด (code) เพื่อทำให้ทดสอบ pass โดยอาจเขียนอย่างง่ายไว้ก่อน
- Refactor: ปรับปรุงโค้ดให้ทำงานได้ดี ถูกต้อง เข้าใจง่าย รวมถึงหากรณีอื่นๆ เพื่อนำไปสู่ testcase ใหม่ให้ครอบคลุมมากขึ้น วนซ้ำไปเรื่อยๆ
เอาล่ะ ต่อไปจะเริ่มลงมือเขียนโค้ดกันเพื่อให้เข้าใจมากขึ้น เริ่มจากสร้างไฟล์ตามนี้
mkdir water
cd ./water
touch water.go
touch water_test.go
ในบทความนี้จะทำการเขียน function เพื่อรับค่า temperature เป็นองศสาเซลเซียส แล้วคืนค่าออกมาเป็นสถานะของน้ำ
🔴Red: เริ่มต้นจากล้มเหลว
เริ่มต้นเขียนจาก testcase ให้ใส่ temperature = 25 แล้วคาดหวังว่าควรจะได้รับคืนค่าเป็น "liquid"
// water_test.go
func TestWaterStatus(t *testing.T) {
temperature := 25
want := "liquid"
got := water.WaterStatus(temperature)
if got != want {
t.Errorf("WaterStatus(%d) = %q; want %q", temperature, got, want)
}
}
🟢Green: แก้ไขให้รันผ่าน
ในที่นี้จะเขียนแบบง่าย หรือโกงให้รันทดสอบให้ผ่านก่อน คือ return "liquid" ไปตรงๆ เลย
// water.go
func WaterStatus(temperature int) string {
return "liquid"
}
รันเทสโดยใช้ go test -v ./...
🔵Refactor: ปรับปรุงจนดีเลิศ
คราวนี้คือส่วนที่ต้องปรับปรุงโค้ดให้มันดีและสมเหตุสมผลมากขึ้น เนื่องจากเราทราบว่าน้ำโดยปกติจะมี 3 สถานะ ได้แก่ solid, liquid และ gas
// water.go
func WaterStatus(temperature int) string {
if temperature <= 0 {
return "solid"
} else if temperature >= 100 {
return "gas"
} else {
return "liquid"
}
}
รันเทสอีกรอบพร้อมเช็ค coverage โดยใช้ go test -v -cover ./...
จะเห็นว่ารันผ่าน แต่ coverage ยังไม่ครบ 100% ทำให้เราต้องกลับมาที่ Red เพื่อเขียน testcase เพิ่ม
กลับมาที่ไฟล์ water_test.go
เราสามารถเขียนแยก function ใหม่สำหรับ case ของ solid และ gas ได้
// water_test.go
func TestWaterStatusLiquid(t *testing.T) {
temperature := 25
want := "liquid"
got := water.WaterStatus(temperature)
if got != want {
t.Errorf("WaterStatus(%d) = %q; want %q", temperature, got, want)
}
}
func TestWaterStatusSolid(t *testing.T) {
temperature := -10
want := "solid"
got := water.WaterStatus(temperature)
if got != want {
t.Errorf("WaterStatus(%d) = %q; want %q", temperature, got, want)
}
}
func TestWaterStatusGas(t *testing.T) {
temperature := 110
want := "gas"
got := water.WaterStatus(temperature)
if got != want {
t.Errorf("WaterStatus(%d) = %q; want %q", temperature, got, want)
}
}
แต่พิจารณาดูแล้วก็เหมือนเขียนโค้ดคล้ายกัน จะดีกว่าไหมหากเราเขียนภายใน function และเปลี่ยนแค่ testcase ที่เราต้องการทดสอบ แบบนี้
// water_test.go
func TestWaterStatus(t *testing.T) {
type testcase struct {
name string
temperature int
expected string
}
cases := []testcase{
{"solid", -10, "solid"},
{"liquid", 25, "liquid"},
{"gas", 110, "gas"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
actual := WaterStatus(tc.temperature)
if actual != tc.expected {
t.Errorf("Expected %s, got %s", tc.expected, actual)
}
})
}
}
ลองรัน go test -v -cover ./...
จะเห็นว่าเป็น pass และ coverage: 100%
หวังว่าการ demo ในบทความนี้จะเป็นประโยชน์สำหรับผู้เริ่มต้นกันนะครับ
Top comments (0)