Shall we Go ตอนที่ 5 – TDD: Test Driven Development

จากตอนที่แล้ว เรามีการสร้าง lib ตัวนึงชื่อว่า stringutil วันนี้เราจะมาเพิ่ม feature ให้กับ lib ตัวนี้ เราจะพัฒนาด้วย TDD: Test Driven Development คือเราจะเขียน Test ก่อน แล้วจึงเขียน Code เพื่อให้ Test ผ่าน

  1. ถ้ายังไม่มี github.com/shall-we-go/stringutil ให้ใช้คำสั่ง $ go get github.com/shall-we-go/stringutil
  2. โครงสร้าง file และ directory จะประมาณนี้:
    $GOPATH/
        src/
            github.com/
                shall-we-go/
                    stringutil/
                        .git/
                        reverse.go
                        reverse_test.go
  3. ตอนนี้เรามี function Reverse() รวมถึง test ของมันคือ TestReverse()
    reverse.go

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    package stringutil
    // Package stringutil contains utility functions for working with strings.
     
    // Reverse returns its argument string reversed rune-wise left to right.
    func Reverse(s string) string {
    	r := []rune(s)
    	for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
    		r[i], r[j] = r[j], r[i]
    	}
    	return string(r)
    }

    reverse_test.go

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    package stringutil
     
    import "testing"
     
    func TestReverse(t *testing.T) {
    	cases := []struct {
    		in, want string
    	}{
    		{"Hello, world", "dlrow ,olleH"},
    		{"Hello, 世界", "界世 ,olleH"},
    		{"", ""},
    	}
    	for _, c := range cases {
    		got := Reverse(c.in)
    		if got != c.want {
    			t.Errorf("Reverse(%q) == %q, want %q", c.in, got, c.want)
    		}
    	}
    }
  4. ลองรันคำสั่ง $ go test github.com/shall-we-go/stringutil เพื่อดูผล test ว่าปกติดังนี้
    ok  	github.com/shall-we-go/stringutil	0.005s
    
  5. มาลองเพิ่ม Test ของ function ใหม่กัน TestUppercase() อย่างที่บอกข้างต้น เราจะเขียน Test ก่อน โดยไฟล์ของ test จะต้องลงท้ายด้วย _test.go เช่น some_function_test.go
    uppercase_test.go

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    package stringutil
     
    import "testing"
     
    func TestUppercase(t *testing.T) {
    	cases := []struct {
    		in, want string
    	}{
    		{"hello", "HELLO"},
    		{"Hello", "HELLO"},
    		{"HELLO", "HELLO"},
    		{"สวัสดี", "สวัสดี"},
    	}
    	for _, c := range cases {
    		got := Uppercase(c.in)
    		if got != c.want {
    			t.Errorf("Uppercase(%q) == %q, want %q", c.in, got, c.want)
    		}
    	}
    }

    อธิบาย code กันหน่อย:

    1
    
    package stringutil

    อย่างที่กล่าวไว่ตอนก่อนหน้าว่า Test จะอยู่ Package เดียวกับ Code ที่จะถูก Test

    3
    
    import "testing"

    Go จะมี Built-in Test Framework มาให้เลย เราแค่ import package ชื่อ testing มาใช้งาน

    5
    
    func TestLowercase(t *testing.T) {

    ประกาศ Test Function โดยชื่อจะต้องขึ้นต้นด้วยคำว่า Test เสมอ, ไม่มี return value ใดๆ, ในส่วนของ Parameter ที่รับก็มีแค่ *testing.T เสมอ เพื่อไว้ส่งผล Test นั่นเอง

    6
    7
    8
    9
    10
    11
    12
    13
    
    	cases := []struct {
    		in, want string
    	}{
    		{"HELLO", "hello"},
    		{"Hello", "hello"},
    		{"hello", "hello"},
    		{"สวัสดี", "สวัสดี"},
    	}

    cases คือรายการ Test Cases ที่เก็บ Input และ Expected Output หรือ Want ของแต่ละ Case, จากตัวอย่างของเรา ทั้ง in และ want เป็น string type, Code อาจจะดูยากนิดนึง จริงๆมันคือการประกาศ Slice (รายการ) ของ Anonymous Struct (โครงสร้างข้อมูล) และกำหนดค่าให้มัน 4 รายการ

    14
    15
    16
    17
    18
    19
    
    	for _, c := range cases {
    		got := Lowercase(c.in)
    		if got != c.want {
    			t.Errorf("Lowercase(%q) == %q, want %q", c.in, got, c.want)
    		}
    	}

    วน loop ที่ละ test case จากนั้นเอา in ส่งให้กับ function ที่จะ test และเอาผลลัพท์ got ไปเปรียบเทียบกับ want, ถ้าไม่ตรงกันก็บันทึก Error

  6. ลองรัน Test จะเจอ error ประมาณนี้ แสดงว่า Test เราพร้อมละ
    # github.com/shall-we-go/stringutil [github.com/shall-we-go/stringutil.test]
    stringutil/uppercase_test.go:15:10: undefined: Uppercase
    FAIL	github.com/shall-we-go/stringutil [build failed]
    
  7. ทีนี้เรามาลองเขียน code เพื่อให้ Test ผ่านกัน
    uppercase.go

    1
    2
    3
    4
    5
    6
    
    package stringutil
     
    // Uppercase returns its argument string in uppercase.
    func Uppercase(s string) string {
    	return s
    }

    เริ่มจาก implement Uppercase() แบบง่ายๆรับ string มาก็ return string ไปตรงๆ, ลองรัน Test จะเห็นว่า Error เปลี่ยนไปแล้ว

    --- FAIL: TestUppercase (0.00s)
        uppercase_test.go:17: Uppercase("hello") == "hello", want "HELLO"
        uppercase_test.go:17: Uppercase("Hello") == "Hello", want "HELLO"
    FAIL
    FAIL	github.com/shall-we-go/stringutil	0.005s

    เพราะเรายังไม่ได้ใส่ logic ของการทำ uppercase เลย ทำให้ test case ไม่ผ่านอยู่ 2 cases, ให้ลองแก้ code ดูดังนี้

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    package stringutil
     
    // Uppercase returns its argument string in uppercase.
    func Uppercase(s string) string {
    	r := []rune(s)
    	offset := 'A' - 'a'
    	for i := range r {
    		if 'a' <= r[i] && r[i] <= 'z' {
    			r[i] += offset
    		}
    	}
    	return string(r)
    }

    ลองรัน Test ใหม่ จะเห็นว่าผ่านแล้ว

สำหรับใครที่ติดใจ TDD อยากฝึกเพิ่มเติมให้ลอง implement Lowercase() ดู โดยให้พัฒนาแบบ TDD คือเขียน Test ก่อนเขียน Code
ติดตามต่อตอนหน้า มาดูเรื่อง Function กัน

Leave a Reply

Your email address will not be published. Required fields are marked *