DEV Community

Cover image for มือใหม่หัด Go: Basic Unit Testing
Nattrio
Nattrio

Posted on

มือใหม่หัด Go: Basic Unit Testing

วันนี้จะมาลองใช้ 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
Enter fullscreen mode Exit fullscreen mode

ในบทความนี้จะทำการเขียน 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)
    }
}
Enter fullscreen mode Exit fullscreen mode

🟢Green: แก้ไขให้รันผ่าน

ในที่นี้จะเขียนแบบง่าย หรือโกงให้รันทดสอบให้ผ่านก่อน คือ return "liquid" ไปตรงๆ เลย

// water.go

func WaterStatus(temperature int) string {
    return "liquid"
}
Enter fullscreen mode Exit fullscreen mode

รันเทสโดยใช้ 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"
    }
}
Enter fullscreen mode Exit fullscreen mode

รันเทสอีกรอบพร้อมเช็ค 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)
    }
}
Enter fullscreen mode Exit fullscreen mode

แต่พิจารณาดูแล้วก็เหมือนเขียนโค้ดคล้ายกัน จะดีกว่าไหมหากเราเขียนภายใน 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)
            }
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

ลองรัน go test -v -cover ./... จะเห็นว่าเป็น pass และ coverage: 100%

หวังว่าการ demo ในบทความนี้จะเป็นประโยชน์สำหรับผู้เริ่มต้นกันนะครับ

Top comments (0)