go-testing-part-2---write-testable-code preview

Go Testing Part 2 - Write Testable Code

การเขียนโค้ดที่สามารถทดสอบได้ (Testable Code) เป็นอีกหนึ่งปัจจัยใจ ที่สามารถแบ่งระดับความชำนาญ/ความเชี่ยวชาญของนักพัฒนาได้ เพราะการมีชุดทดสอบที่ดี เชื่อถือได้ จะทำให้การพัฒนา Software เป็นไปได้อย่างราบรื่น

สำหรับนักพัฒนาที่ทำงานมาได้สักระยะจะรู้กันดี ว่าการที่ระบบของเรามีชุดทดสอบที่พึ่งพาได้ จะช่วยให้เราทำงานได้มีความสุขแค่ไหน นอกเหนือจากเหตุผลด้าน KPI ของทีม การมีชุดทดสอบที่ดี จะทำให้นักพัฒนาในทีมทำงานได้อย่างมีประสิทธิภาพ แก้โค้ดได้อย่างมั่นใจ

Dependency Injection

ในภาษา Go มีหนึ่งรูปแบบการเขียนโค้ดให้เหมาะกับการทดสอบ โดยอาศัยหลักการ Dependency Injection (DI) และความสามารถของ interface มาช่วยในการทำให้โค้ดเป็น Testable Code ได้

Dependency Injection เป็นหลักการที่เราจะส่ง dependency เข้าไปใน function หรือ method แทนที่จะสร้าง dependency ใน function หรือ method นั้นๆ ขึ้นมาเอง ซึ่งจะช่วยเพิ่มความยืดหยุ่นของการทำงาน โดยที่เราสามารถเปลี่ยน dependency ได้ตามต้องการ โดยไม่ต้องแก้โค้ดที่เราเขียนไปแล้ว ขอแค่มี interface ที่เป็นตัวกลางในการรับ dependency ที่เราต้องการ

มาดูตัวอย่าง Code ที่ใช้ interface ช่วยในการทำ Dependency Injection กัน

package main

// UserWriter เป็น interface แปลว่าสามารถใช้ type ใดใดก็ตาม ที่มีการ implement method Write(user User) error ทดแทนกันได้
type UserWriter interface {
    Write(user User) error // interface นี้มี method write ที่รับ user เป็น parameter และ return error
}

type UserHandler struct {
    userWriter UserWriter // dependency ของเรา สร้าง field ชื่อ userWriter เป็น type UserWriter ที่เป็น interface
}

func (u *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
    // do stuff before writing
    u.userWriter.Write(user) // เรียกใช้ method Write ผ่าน userWriter
    // ...
}

type UserWriterImpl struct {
    db *sql.DB
}

func (u *UserWriterImpl) Write(user User) error {
    // ทำการ implement method Write ที่รับ user เป็น parameter และ return error
    return nil
}

// จากนี้ไป UserWriterImpl จะสามาถใช้แทน UserWriter ได้แล้ว เพราะมี method Write อยู่

func main() {
    db, err := getSomeDB()
    if err != nil {
        log.Fatal(err)
    }

    userWriter := &UserWriterImpl{db: db}
    userHandler := &UserHandler{userWriter: userWriter} // <- inject userWriter เข้าไปใน userHandler
    http.HandleFunc("/users", userHandler.CreateUser)
}

จะเห็นว่า UserHandler นั้นมีสมาชิก 1 ตัวคือ userWriter type UserWriter ซึ่งเป็น interface ที่มี method Write

จากนั้น มีการสร้าง type UserWriterImpl เป็น struct ที่มีการ implement method Write ทำให้ตอนนี้ เราสามารถส่ง UserWriterImpl เข้าไปใน UserHandler ได้ จะสามารถเรียกใช้ method Write ผ่าน userWriter ได้ละ

มาถึงตรงนี้ แปลว่า เราสามารถที่จะประกาศ type อะไรก็ตามที่มี method Write เพื่อที่จะส่ง type นั้นเข้าไปใน UserHandler ได้ ซึ่งเราสามารถใช้ได้กับการทดสอบได้เลย

package main

type MockUserWriter struct {
    isCall bool
}

func (m *MockUserWriter) Write(user User) error {
    m.isCall = true
    return nil
}

func Test_CreateUser(t *testing.T) {
    mockUserWriter := &MockUserWriter{}
    userHandler := &UserHandler{userWriter: mockUserWriter}

    userHandler.CreateUser(nil, nil)

    if !mockUserWriter.isCall {
        t.Error("expect to call Write method")
    }
}

Mock Tool

หลักจากเราลองเขียน mock type ขึ้นมาเองแล้ว อาจจะดูลำบากหน่อยถ้ามี interface จำนวนมากที่เราต้องเขียน mock type ขึ้นมาเอง เรามาดูตัวช่วยกันดีกว่า นั่นก็คือ mockery ซึ่งเป็น tool ที่ช่วย generate mock type ให้เราอ้างอิงจาก interface ที่เราสนใจ

อ่านรายละเอียดเพิ่มเติม https://github.com/vektra/mockery

go install github.com/vektra/mockery@latest

mockery --name UserWriter --with-expecter --inpackage

แล้วเราก็จะได้ไฟล์ mock_UserWriter.go ที่มีการ mock type UserWriter ให้เราเรียบร้อย พร้อมกับ implementation ต่างๆที่เป็นประโยชน์ในการทดสอบ

package main

import (
    "testing"
)

func Test_CreateUser(t *testing.T) {
    mockUserWriter := NewMockUserWriter(t)
    userHandler := &UserHandler{userWriter: mockUserWriter}

    mockUserWriter.EXPECT().Write(User{Name: "John"}) // เราสามารถ expect ได้ว่า Write จะถูกเรียกด้วย User ที่มี Name เป็น John

    userHandler.CreateUser(nil, nil)
}

เครื่องมือที่เราใช้ในการช่วยทำ Mock Dependency ก็จะมีประมานนี้ หากเราออกแบบโปรแกรมให้สามารถเทสได้ง่ายตั้งแต่แรก การเขียน unit test ก็จะไม่ใช่เรื่องน่าปวดหัวอีกต่อไป

Summary

ขอให้สนุกกับการเขียนโค้ดครับ

← เพิ่มประสิทธิภาพโปรแกรมภาษา Go ด้วยเทคนิก Memoization แก้ปัญหา Node.js Projects ใช้หลาย version ด้วย volta.sh →