เริ่มทำ Unit Test ใน Golang กันเถอะ!
Unit Test คืออะไร?
Unit Test คือการทดสอบซอฟท์แวร์ประเภทหนึ่งที่ทำการทำสอบในระดับเล็ก (Unit) หรือระดับ Component โดยเป้าหมายเพื่อที่จะทดสอบการทำงานในแต่ละส่วนของซอฟท์แวร์ว่าทำงานได้ตามความต้องการหรือไม่ ซึ่ง Unit Test จะเกิดขึ้นในช่วงที่พัฒนาซอฟท์แวร์ของ Developer โดยจะแยกโค๊ดในแต่ละส่วนออกจากกันเพื่อตรวจสอบความถูกต้อง โดย Unit อาจจะหมายถึง Function หรือ โมดูลก็ได้ แล้วแต่คนจะนิยาม
ทำไมต้องทำ Unit test?
เวลาที่เราพัฒนาซอฟท์แวร์เนี่ยช่วงแรก ๆ ที่เราเขียนฟังก์ชั่นคือโมดูลต่าง ๆ มันก็ดูปกติดี ทดสอบก็ง่าย แต่เมื่อทำไปเรื่อย ๆ จำนวนเคสที่จะต้องทดสอบหรือความซับซ้อนของโปรแกรมก็มากขึ้นทำให้เมื่อเราทดสอบด้วยตัวเองแล้ว เราอาจจะหาจุดบกพร่องไม่เจอหรืออาจจะหลุดการทดสอบในบางเคสไป ซึ่ง Unit Test จะเข้ามาช่วยในเรื่องนี้ ด้วยความที่การทำ Unit Test จะทดสอบจากส่วนเล็ก ๆ ของโปรแกรม ทำให้เมื่อส่วนใดมีปัญหาก็สามารถรู้ได้ทันที อีกทั้งเวลาที่เรากลับมาเพิ่มความสามารถให้กับฟังก์ชั่นหรือโมดูลเดิม เราก็สามารถรัน Unit Test อันเดิม เพื่อตรวจสอบการทำงานว่าที่เราแก้ไขไปนั้น กระทบอะไรกับการทำงานเดิมหรือไม่ ซึ่งทำให้เราได้ซอฟท์แวร์ที่มีคุณภาพมากขึ้นและยังประหยัดเวลาในการทดสอบซ้ำ ๆ อีกด้วย
มาเริ่มเขียน Unit Test กันเถอะ!
ใน GoLang จะมี package สำหรับทำ testing ซึ่งไว้ใช้สำหรับเขียน test ติดมาอยู่แล้วทำให้เราสามารถเขียน test ได้เลย ไม่ต้องติดตั้งอะไรเพิ่ม
มาลองสร้างโปรแกรมง่าย ๆ ขึ้นมากันก่อนดีกว่า
ก่อนจะเขียน Test เราก็ต้องมีโปรแกรมที่อยากจะ Test ก่อน งั้นมาเริ่มกันเลย สร้างไฟล์ main.go
ขึ้นมาแล้วเขียนโปรแกรมสักอันนึง
mkdir ./gotest
mkdir ./gotest/src
cd ./gotest/src
touch main.go
code main.go
package main
func Multiply(a int, b int) int {
return a * b
}
ในตัวอย่างด้านบนเราสร้างฟังชั่นสำหรับคูณเลขขึ้นมา พอเราได้โค๊ดที่เราต้องการจะ Test มาแล้วเวลาที่เราจะเขียน Test เราก็จะสร้างไฟล์ใหม่ขึ้นมาซึ่งจะตั้งชื่อโดยอ้างอิงจากชื่อไฟล์ที่เราต้องการทดสอบ เช่นในที่นี้เรามีไฟล์ main.go
ไฟล์ Test ก็จะเป็น main_test.go
ในไฟล์ main_test.go
เราจะใช้สำหรับเขียนโค๊ดที่เกี่ยวข้องกับการ Test ให้ทำการ Import package 'testing'
เข้ามาเลย จากนั้นเราก็เริ่มเขียนฟังก์ชั่นสำหรับ Test
ข้อควรรู้: ใน Golang การเขียน Test ชื่อฟังก์ชั่นที่ใช้สำหรับทดสอบจะต้องเป็นไปตาม Format ที่กำหนดเท่านั้น
func Test___(t *testing.T)
โดยชื่อจะต้องขึ้นต้นด้วย Test
และตามด้วยชื่อของฟังก์ชั่นที่ต้องการจะ Test โดยฟังก์ชั่นนี้จะรับ parameter 1 ตัว นั่นคือ *testing.T
โดยพารามิเตอร์นี้จะมีฟังก์ชั่นในการทำ error reporting หรือ logging ให้ใช้งานด้วย
package main
import "testing"
func TestMultiply(t *testing.T) {
got := Multiply(5, 2)
want := 10
if got != want {
t.Errorf("expect '%d', got '%d'", want, got)
}
}
จากโค๊ดด้านบนจะอธิบายได้ดังนี้ ตัวแปร got
จะรับผลลัพท์จากการเรียกใช้งานฟังก์ชั่น Multiply มาเก็บไว้ โดยในเคสนี้เราเรียกฟังก์ชั่นนี้โดยส่งค่า 5
กับ 2
ไป
ในส่วนของ want
นั้นจะเป็นการกำหนดค่าที่เราคาดหวังว่าจะได้ออกมาจากฟังก์ชั่นที่เราต้องการทดสอบ ซึ่งในเคสนี้ เราส่ง 5
และ 2
ไปและเราคาดหวังว่าจะได้ 10
กลับมา (5x2 = 10)
ในส่วนถัดไปเราก็ทำการเปรียบเทียบค่าว่าหาก got
ไม่เท่ากับ want
ก็ให้โยน Error ออกไปว่า Test failed
หลังจากนั้นมาลองรัน Test อันนี้กันเถอะ
ลองรันคำสั่ง go test
ดูกันเลย!
PS C:\Users\speed\OneDrive\Desktop\gotest\src> go test
PASS
ok gotest/src 0.175s
ใน Terminal ก็จะแสดงผลลัพท์ของการทดสอบออกมาซึ่งในที่นี้ก็ทดสอบได้ผ่าน
การรันคำสั่ง go test
จะเป็นการทดสอบทั้งหมด ซึ่งหากต้องการระบุไฟล์ที่ต้องการทดสอบก็สามารถทำได้โดยใช้คำสั่ง go test ./package_name
โดยระบุ path ของ package ที่จะทดสอบได้ นอกจากนี้หากต้องการแสดงรายละเอียดการทดสอบก็ยังสามารถเพิ่ม -v
เข้าไปได้ ซึ่งจะทำให้แสดงชื่อฟังก์ชั่นออกมาด้วย ในกรณีที่ในไฟล์นั้นมีหลายฟังก์ชั่นก็ทำให้สะดวกขึ้น
PS C:\Users\speed\OneDrive\Desktop\gotest\src> go test -v
=== RUN TestMultiply
--- PASS: TestMultiply (0.00s)
PASS
ok gotest/src 0.180s
ทีนี้เรามาลองเพิ่มฟังก์ชั่นบวกเลขกันดีกว่า
### main.go
package main
func Multiply(a int, b int) int {
return a * b
}
func Add(a int, b int) int {
return a + b
}
package main
import "testing"
func TestMultiply(t *testing.T) {
got := Multiply(5, 2)
want := 10
if want != got {
t.Errorf("expect '%d', got '%d'", want, got)
}
}
func TestAdd(t *testing.T) {
got := Add(1, 2)
want := 3
if want != got {
t.Errorf("expect '%d', got '%d'", want, got)
}
}
เมื่อเรารัน go test -v
จะเห็นได้ว่าจะแสดงผลลัพท์ตามรายฟังก์ชั่นที่ทำการทดสอบเลย
PS C:\Users\speed\OneDrive\Desktop\gotest\src> go test -v
=== RUN TestMultiply
--- PASS: TestMultiply (0.00s)
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok gotest/src 0.181s
งั้นมาลองแก้ให้ Test failed กันดีกว่า จะออกมาเป็นยังไงกันนะ ลองแก้ตัว want
ของฟังก์ชั่น TestMultiply จาก 10
กลายเป็น 5
ดูกันเถอะ
package main
import "testing"
func TestMultiply(t *testing.T) {
got := Multiply(5, 2)
want := 5
if want != got {
t.Errorf("expect '%d', got '%d'", want, got)
}
}
func TestAdd(t *testing.T) {
got := Add(1, 2)
want := 3
if want != got {
t.Errorf("expect '%d', got '%d'", want, got)
}
}
เมื่อรันเทสออกมาแล้ว ก็จะ FAIL ทันทีและก็แสดงข้อความ error ตามที่เราได้เขียนเอาไว้เลยว่า คาดหวังว่าฟังก์ชั่นนี้จะคืนค่าเป็น 5 แต่กลับได้ 10 มา
PS C:\Users\speed\OneDrive\Desktop\gotest\src> go test -v
=== RUN TestMultiply
main_test.go:10: expect '5', got '10'
--- FAIL: TestMultiply (0.00s)
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
FAIL
exit status 1
FAIL gotest/src 0.180s
แต่แน่นอนว่าฟังชั่นก์อันนึงของเรา ในความเป็นจริงแล้วเราไม่ได้ Test แค่เคสเดียวต่อฟังก์ชั่นแน่ ๆ เพราะงั้น จะทำยังไงล่ะ ให้เราทดสอบหลาย ๆ เคสได้ ซึ่งใน go เองก็มีคำสั่ง t.Run ซึ่งเป็นคำสั่งสำหรับการรัน sub test หรือ test case ย่อย ๆ จากตัว Test หลักอีกทีนึง พูดแล้วไม่เห็นภาพ มาลองดูกันเลยดีกว่า
func TestAdd(t *testing.T) {
type testCase struct {
name string
arg1 int
arg2 int
want int
}
tests := []testCase{
{ "2 + 2 should be 4", 2, 2, 4},
{ "2 + 3 should be 5", 2, 3, 5},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Add(tt.arg1, tt.arg2)
if got != tt.want {
t.Errorf("Add() = %v, want %v", got, tt.want)
}
})
}
}
จากภาพด้านบน เราจะประกาศ struct สำหรับเก็บข้อมูลแต่ละ case ของเราที่จะทำการทดสอบมา โดยในนั้นจะมี name ก็คือชื่อ test case นั้น ๆ และมี arg1, arg2 ซึ่งเป็นค่าที่จะส่งเข้าไปในฟังก์ชั่น และ want ก็คือผลลัพท์ที่คาดหวัง หลังจากนั้นเราก็กำหนด test case ขึ้นมา และนำมา for loop เพื่อเรียกฟังก์ชั่น t.Run เพื่อทำการรัน sub test ตามที่เรากำหนด
PS C:\Users\speed\OneDrive\Desktop\gotest\src> go test -v
=== RUN TestMultiply
--- PASS: TestMultiply (0.00s)
=== RUN TestAdd
=== RUN TestAdd/2_+_2_should_be_4
=== RUN TestAdd/2_+_3_should_be_5
--- PASS: TestAdd (0.00s)
--- PASS: TestAdd/2_+_2_should_be_4 (0.00s)
--- PASS: TestAdd/2_+_3_should_be_5 (0.00s)
PASS
ok gotest/src 0.179s
ผลลัพท์ก็จะตามภาพด้านบนเลย โดยหากมี sub test อันไหนที่ fail ก็จะออกมาประมาณนี้
PS C:\Users\speed\OneDrive\Desktop\gotest\src> go test -v
=== RUN TestMultiply
--- PASS: TestMultiply (0.00s)
=== RUN TestAdd
=== RUN TestAdd/2_+_2_should_be_4
=== RUN TestAdd/2_+_3_should_be_8
main_test.go:32: Add() = 5, want 8
--- FAIL: TestAdd (0.00s)
--- PASS: TestAdd/2_+_2_should_be_4 (0.00s)
--- FAIL: TestAdd/2_+_3_should_be_8 (0.00s)
FAIL
exit status 1
FAIL gotest/src 0.181s
Test Coverage
เราจะรู้ได้ยังไงว่า Test case ที่เราเขียนไปมันครอบคลุมทุกการทำงานของฟังก์ชั่นนั้น ซึ่ง go testing มีเรื่องนี้มาให้อยู่แล้ว เพียงเติม -cover
ต่อหลังไปก็จะได้ผลลัพท์แบบนี้เลย
PS C:\Users\speed\OneDrive\Desktop\gotest\src> go test -cover
PASS
coverage: 100.0% of statements
ok gotest/src 0.181s
coverage: 100.0% of statements ซึ่งก็แปลว่าเทสทั้งหมดของเรา รันโค๊ดของฟังก์ชั่นนั้นครบทุกบรรทัด
มาลองเพิ่มเงื่อนไขให้มันไม่ครอบคลุมกันดีกว่า ลองแก้โค๊ดดู
package main
func Multiply(a int, b int) int {
return a * b
}
func Add(a int, b int) int {
if a == 0 {
return 0
}
return a + b
}
ทีนี้ลองรัน go test -cover
PS C:\Users\speed\OneDrive\Desktop\gotest\src> go test -cover
PASS
coverage: 75.0% of statements
ok gotest/src 0.184s
เหลือ 75% ซะแล้ว แบบนี้แปลว่ามีโค๊ดที่ไม่ได้ถูกรันจาก test case ตัวไหนเลยซึ่งหมายความว่า test case ไม่ครอบคลุมนั่นเอง
คำถามถัดไป แล้วจะรู้ได้ไงว่าตรงไหนไม่ครอบคลุม ?
testing ของ go สามารถสร้าง report ให้เราดูข้อมูลได้ โดยใช้คำสั่งตามนี้
go test -coverprofile cover.out
เมื่อรันแล้วเราก็จะได้ไฟล์ cover.out
ออกมาซึ่งเป็นไฟล์ที่เก็บข้อมูลของ code coverage ออกมา
mode: set
gotest/src/main.go:3.33,5.2 1 1
gotest/src/main.go:7.28,8.12 1 1
gotest/src/main.go:11.2,11.14 1 1
gotest/src/main.go:8.12,10.3 1 0
อ่านไม่รู้เรื่องเลยทำไงดี ?
แน่นอนว่า go เองก็มี tool ที่เอาไฟล์ cover.out
ของเรานี่มาแสดงผลให้ดูได้โดยใช้คำสั่งนี้
go tool cover -html="cover.out"
ผลลัพท์ก็สวยสดงดงามตามนี้เลย เห็นชัดเจนว่าโค๊ดตรงไหนที่ไม่ได้ถูกรัน test
เป็นยังไงกันบ้างครับสำหรับการทำ Unit test บน go หวังว่าจะเป็นประโยชน์สำหรับผู้เริ่มต้นไม่มากก็น้อยนะครับ เพราะการเขียนเทสเป็นเครื่องมือการันตีว่าซอฟท์แวร์ของเราจะทำงานได้อย่างที่คาดหวัง อีกทั้งยังนำมาทดสอบซ้ำได้เมื่อมีการแก้ไขฟังก์ชั่นนี้ในอนาคตซึ่งจะช่วยให้ลดเวลาในการทำงานได้อีกด้วย
📢 มาร่วมเป็นส่วนหนึ่งในการทำให้วงการ E-Commerce ขับเคลื่อนไปข้างหน้า ส่งประวัติการทำงานพร้อมตำแหน่งงานที่คุณสนใจมาได้เลยที่อีเมล hr@sellsuki.com หรือเข้าชมเว็บไซต์ของเราที่ https://lnkd.in/gUqNHSEW 🐶