เริ่มทำ Unit Test ใน Golang กันเถอะ!

Ekapol Tassaneeyasin
Stories of Sellsuki
4 min readDec 23, 2022

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 🐶

--

--