ทำไมถึงต้องเขียนเทส
เหตุผลหลักๆ ที่ในโปรแกรมของเราควรมีการเขียนเทสก็คือ เพื่อให้มั่นใจว่าโปรแกรมของเรานั้นทำงานได้ถูกต้อง เมื่อมีการปรับปรุงหรือแก้ไขโค๊ดบางส่วน เราก็ยังสามารถเชื่อถือได้ว่าส่วนที่เราแก้ไป ไม่ได้ส่งผลกระทบทำให้โค๊ดในส่วนอื่นๆ ของโปรเจคเรามีปัญหา
ในกรณีที่เรามีการเขียนโค๊ดโดยใช้หลักการ TDD หรือง่ายๆ คือการเขียนเทสก่อนเขียนโค๊ด ยิ่งจะช่วยให้เราสามารถเขียนโปรแกรมได้ไวขึ้น และสามารถทดสอบและหาบัคได้อย่างรวดเร็วและน่าเชื่อถือมากยิ่งขึ้น
มาเริ่มกันเลย
ก่อนอื่นเรามาดูโครงสร้างของโปรเจคกันก่อน จากใน repo นี้จะเห็นว่ามีโครงสร้างของโปรเจคหน้าตาดังนี้
numbertowords
+-- main.go
| +-- func NumToWords(number int) string
+-- README.md
Copy
จะเห็นว่าในไฟล์ main.go มีฟังก์ชันที่ชื่อว่า NumToWords ซึ่งมีอินพุตเป็น int และเอ้าท์พุตเป็น string ใช้สำหรับการแปลงเลขให้กลายเป็นคำ เรามาเริ่มเขียนเทสกันเลย
ให้เราทำการสร้างไฟล์ใหม่ขึ้นมาภายใต้โปรเจคโดยใช้ชื่อว่า main_test.go จากนั้นเรามาลองเขียนโค๊ดกัน ตามนี้
package numbertowords
import (
"fmt"
"log"
"testing"
)
func TestNumToWords(t *testing.T) {
var tests = []struct {
input int
want string
}{
{input : 1 , want : "หนึ่ง"},
{input : 12 , want : "สิบสอง"},
{input : 123 , want : "หนึ่งร้อยยี่สิบสาม"},
{input : 1234 , want : "หนึ่งพันสองร้อยสามสิบสี่"},
{input : 12345 , want : "หนึ่งหมื่นสองพันสามร้อยสี่สิบห้า"},
{input : 123456 , want : "หนึ่งแสนสองหมื่นสามพันสี่ร้อยห้าสิบหก"},
{input : 1234567 , want : "หนึ่งล้านสองแสนสามหมื่นสี่พันห้าร้อยหกสิบเจ็ด"},
{input : 10 , want : "สิบ"},
{input : 101 , want : "หนึ่งร้อยเอ็ด"},
{input : 1011 , want : "หนึ่งพันสิบเอ็ด"},
{input : 10111 , want : "หนึ่งหมื่นหนึ่งร้อยสิบเอ็ด"},
{input : 101111 , want : "หนึ่งแสนหนึ่งพันพนึ่งร้อยสิบเอ็ด"},
{input : 1011111 , want : "หนึ่งล้านหนึ่งหมื่นหนึ่งพันหนึ่งร้อยสิบเอ็ด"},
}
for _, tt := range tests {
testName := fmt.Sprintf("%s", tt.want)
t.Run(testName, func(t *testing.T) {
ans := NumToWords(tt.input)
if ans != tt.want {
t.Errorf("got : %s, want : %s", ans, tt.want)
}
})
}
}
Copy
อธิบายโค๊ดเบื้องต้น
เรามีการประกาศตัวแปร tests เป็นสตรัคแอเรย์ ใช้ในการเก็บค่า input และค่า want (คำตอบที่เราจะใช้เปรียบเทียบ) ซึ่งเราจะเขียนเคสแต่ละเคสลงมาในนี้ ซึ่งจากโปรเจคตัวอย่าง ยังรองรับแค่หลักล้าน เพราะฉะนั้นเราจะเขียนเทสกันถึงแค่ระดับหลักล้าน
ส่วนต่อมาจะเป็นส่วน for loop เพื่อทำการรับค่าจากตัวแปร tests มาทำการวนลูปเข้าฟังก์ชัน NumToWords และทำการเปรียบเทียบค่าที่ได้จากเอาท์พุตของฟังก์ชั่น เทียบกับตัวแปร want ขึ้นตรงกับ input นั้นๆ
เสร็จแล้ว ลองรันดูเล้ยยย โดยใช้คำสั่ง go test ./… โดย ./… นั้น จะทำให้ตัว go วิ่งไปค้นหาไฟล์เทสทั้งหมดที่อยู่ในโปรเจคของเรามา test ได้ผลดังนี้
$ go test ./...
ok _/D_/go_path/numbertowords 0.236s
Copy
เรียบร้อยจะเห็นว่าโปรแกรมของเราทดสอบผ่านทุกตัว
ถ้าในกรณีที่เทสของเราไม่ผ่านหน้าตาจะออกมาเป็นอย่างไร?
เรามาลองแก้ไขเทสเคสบางตัวให้ไม่ถูกต้องเพื่อดูผลว่าเทสไม่ผ่านเป็นอย่างไรกัน โดยแก้ไข ดังนี้
{input : 123 , want : "หนึ่งร้อยยี่สิบสาม"},
//แก้บรรทัดนี้เป็น
{input : 123 , want : "หนึ่งร้อยสองสิบสาม"},
Copy
ลองรันกันเล้ยยยยย
$ go test ./...
--- FAIL: TestNumToWords (0.00s)
--- FAIL: TestNumToWords/หนึ่งร้อยสองสิบสาม (0.00s)
main_test.go:35: got : หนึ่งร้อยยี่สิบสาม, want : หนึ่งร้อยสองสิบสาม
FAIL
FAIL _/D_/go_path/numbertowords 0.140s
Copy
จะเห็นว่ามีการเฟลเกิดขึ้นที่ หนึ่งร้อยสองสิบสาม (อันนี้เราสมมติขึ้นมาเฉยๆนะครับ โปรแกรมมันไม่ได้มีบัคจริงๆ) ทำให้เราสามารถดีบัคเพื่อทำการตรวจสอบโค๊ดของเราได้ตรงจุดว่ามีปัญหาที่ตรงไหนและสามารถแก้ไขข้อผิดพลาดได้อย่างรวดเร็ว
ฝากทิ้งทายไว้นิดนึง
การเขียนเทสมากไปก็ไม่ใช่ว่าจะดีเสมอไป หากการเขียนเทสนั้นเป็นการเทสแบบซ้ำๆ หรือเทสไม่ถูกจุดเราก็จะเสียเวลาเทส เสียเวลาในการดีพลอยงานไปฟรีๆ
ส่วนการไม่เขียนเทสเลยอันนี้ไม่ดีแน่นอนเพราะถ้าหากเราเทสเองแบบแมนนวล เราจะหลุดเทสอาจเทสไม่ครบทุกเคสก็ได้ หรือท้ายที่สุดเรามีการแก้ไขโค๊ด แต่เราไม่มีเทส อาจจะทำให้เราเสียเวลาอย่างมากในการหาว่าจุดไหนที่มีปัญหา ซึ่งหากเรามีเทสและเทสนั้นครอบคลุมมากพอ เราจะสามารถบอกได้ทันทีว่าโปรแกรมของเราเมื่อแก้แล้วส่งผลกระทบที่จุดไหน