[KBTG-GO#03] Software Testing

0. Go Basic

เหมือนมันมา Go Testing เลย ลองอะไรแล้วสงสัยมาแปะในนี้ และกัน แต่อาจจะมีแซมๆที่จุดอื่นบ้าง และก็ Go เป็นภาษาที่ใช้ PascalCase มีกฏระดับนึง แต่อาจจะไม่ Strick เท่ากับ Space ของ Python

- Package
  • กลุ่มของ Code คล้ายกับ namespace ใน c# และเข้าใจว่า ถ้า main app ต้องอยู่ใน package main นะ
  • ชื่อ Folder ตั้งเหมือนกันชื่อ Package
  • ลองดูใน Create Go 101
- Variable + Operator + Control Flow
//With DataType
var i int = 42
var f float64 = float64(i)

//Type inference แบบอะไรก็ได้ใน js ส่วน dotnet น่าจะ var / dynamic
var name = "PingkungA"
var age = 32

//Short Hand 
name := "PingkungA"
age := 32
  • Operator เหมือนกับภาษาอื่นๆ มี Arithmetic / Logical ไงงี้
  • Control Flow - if บังคับปีกกาเปิด ถ้าตบลงมาเหมือนจะ Build ไม่ผ่าน
if x < 0 {
   return sqrt(-x) + "i"
}
- Function
  • Pattern
    ถ้าต้องการ public ต้อง Upper Case ตัวแรก
    - Sum -> package อื่นเห็น
    - sum -> private นะ
    Idea นี้ เอาไปใช้กับพวก Struct ด้วย อารมณ์ Get / Set ของ C#
func NAME_OF_FUNC (input) (output) 
{
   //YOUR_LOGIC
}
  • เขียนได้หลายแบบ
// The following three function types are identical.
func () (x int)
func () (int)
func () int

// The following two function types are identical.
func (a int, b string) ()
func (a int, b string)
- Loop
  • เหมือนจะมีแต่ for นะ แต่เหมือน 1.22+ จะมีแบบสั้น
for i:=1; i<= 10; i++ {
   //Your Logic
}

//Go 1.22+
for i:=1 range 11 {
   //Your Logic
}
  • ส่วนแบบอื่นๆ do .. while / while / infinite loop ให้ apply จาก for เอา //เออง่ายดี
  • อ๋อมี Variadic function อยู่ใน Blog นี้แหละ ย้ายไปย้ามมา แล้วมันแปลกๆเลยไว้ที่เดียว
- Pointers

เหมือนย้อนกลับมาเรียน C ยังไงไม่รู้ 555 หลังๆภาษาพวก C# / Java มันจัดการเอง

  • Pass by Value - Copy Value ส่งไป
  • Pass by Reference - เอา Memory Reference ส่งไป
//Declare Pointer
var p *int

i := 42
p = &i //ให้ตัวแปร p ไปจิ้ม Memory จุดเดียวกัน i

//================================================
//Pass by Value (Copy Value pointer ลงค่าใหม่)
doublePtr(n)

//Pass by Reference (ค่าของ pointer) ใช้ & ส่งตำแหน่งไป 
doublePtr(&n)

//Function
func doublePtr(n *int) {
 *n *= 2
 }

1. Go Testing

Convention

  • Go เป็นภาษาที่ใช้ PascalCase
  • ชื่อไฟล์ _Test.go
  • ไฟล์ test อยู่ข้าง Code Logic เลย
    - มองภาพ แล้วมันจะขัดๆ กับ C# Java มันออกมาเป็นอีก Project ตั้ง namespace ให้ล้อกัน
    - แต่ก็ดีไปอีกแบบหาง่าย
  • ชื่อ Func TestXXX ขึ้นต้อนด้วย Test เสมอ โดยมี Signature จะเป็น (T *testing.T) //อารมณ์เหมือน C Pointe
Create Go 101

Enviroment ใช้เยอนะ ส่องการตั้งค่าประมาณนี้

mac/linux
export GOROOT=~/go1.x.x
export GOBIN=$GOPATH/bin
export PATH=$GOBIN:$GOROOT/bin:$PATH
windows
set GOROOT=C:\go1.x.x
set GOBIN=%GOPATH%\bin
set PATH=%GOBIN%;%GOROOT%\bin;%PATH

Create Go 101

GoTest
    │   go.mod
    │   main.go  //เข้าใจว่าเค้าอยากให้ TDD แหละ แต่มันไม่เห็นภาพ พอลองแล้ว หงุดหงิดสุด เรื่อง import 555
    │
    └───sum
            sum.go
            sum_test.go
  • สร้างไฟล์ go mod
go mod init github.com/<username>/<repo_name> //ตั้งแบบนี้ เวลาคนอื่นเอาไปใช้ มันจะวิ่งไปดึง Code ไปให้ 

//สร้างเสร็จ run 
go mod tidy
  • ข้างในไฟล์ go mod ในหัวผมตอนนี้ มันจะอารมณ์แบบพวก pom / csproj นะ ไม่รู้ว่าถูกไหม แต่ปักไปก่อน
module github.com/pingkunga/learn-go

go 1.21.4
  • สร้างไฟล์ sum.go //Function ถ้าต้องการ public ต้อง Upper Case ตัวแรก
    - Sum -> package อื่นเห็น
    - sum -> private นะ
  • สร้างไฟล์ sum_test.go
    - สร้าง Test Fuction ขึ้นมา ถ้ากลัวมันซ้ำ ก็เอา ทำ SubTest t.run
    - 3A
    > Arrange - เตรียมทุกอย่าง ปั๊นอะไรมา กำหนด Expected
    > Act - เรียก Func ที่ต้องการ Test ในที่นี้ก็ Sum
    > Assert - มันดูตรงๆดีนะ ไม่มี Assert อยากได้อะไร If เอา

SubTest t.run บราๆ มันดู Copy แปะ ๆ ยังไงไม่รู้

ฝั่ง dotnet มันมี DataRow และ @ParameterizedTest ของ Java
เหมือนมี testing - go - golang test parameterized - Stack Overflow เดวธาตุไฟแทรก 55

Test Command

go test . run ใน dir
go test ./ ... เอาไส้หมด
go test -v . บอกให้พ่น log
go test -run <TestName จะเป็น RegEx>

แต่กดจาก VS Code ก็ได้นะ ฮ่าๆ

มันผ่านแล้วแหละ แต่อยากลองเดิมๆ อาจจะขัดใจสาย TDD ลอง main มัน Run จะไปงงตอน Import

  • Go ไม่รู้จัก relative path นะ มันต้องไปใช้ path ใน go mod
  • main ต้องอยู่ใน package main ด้วยมั้ง
github.com/<username>/<repo_name>/<PACKAGE_NAME>
  • ตัว main ที่ได้ จะประมาณนี้ main.go โดยตอน Run ใช้ Command
go run <path_to_main.go>
  • Variadic function รับตัวแปร พวก array มา spread ได้แบบใน js เหมือนของ dotnet ก็เพิ่งมีมานะ
    - Go - range function
func SumVariadic(xs ...int) int {
  var total int
  //foreach ในภาษาอื่นๆ แหละ 
  // _ บอก current index 
  // แต่ละ Element เก็บค่าลงตัวแปร num ใน Loop แต่ละรอบ 
  for _, num := range xs {
     total += num
  }
  return total
}

แปะไว้ เผื่ออนาคต learn-go/SoftwareQuality/sum at main · pingkunga/learn-go (github.com)

2 Testing Techniques

เอามาตอบว่าจะเอาอะไรมาเทส โดยมี 2 เทคนิค + ประสบการณ์ อารมณ์ย้อนไปเรียน SW Testing ตอน ป โท เลย

  • Boundary Value ต้องงจะข้อมูลเป็น Range ก่อน (อะไรที่มองไม่เป็นตัวเลขต้องมองในรูปนี้) แล้ว เลือก
    - min
    - min +1
    - norm ค่ากลาง
    - max
    - max +1
  • Decision Table - Condition + Possibility
  • ที่เหลือมองเป็นการฝึก + ประสบการณ์ เช่น
    - Divide by Zero / Null Pointer
    - ไม่ใส้ข้อมูล
    - Business เช่น อายุ -1 มีไหม ดักจาก UI / DataType (เรื่อง Data Type Sign/Unsign ไม่เคยคิดเลย 555)
    - เป็นต้น

มาลองกัน ตอนนี้รู้เรื่อง If และ

package ticket
#uint = unsign integer
func Price(age int) float64 {
	if age <= 10 {
		return 0
	} else if age < 18 && age > 60 {
		return 0.5
	} else {
		return 1.5
	}
}

เรื่องของ Price TestPrice

3 Test Pyramid

Anti-Pattern ICE CREAM CONE / CUPCAKE เน้นงาน Manual เยอะๆ ชอบม๊ากกก //ประชด 555

Test Pyramid - Automate เยอะ / Manual น้อย

  • Simple
    - Unit Tests - มีเยอะๆ ทดสอบ Method / Function
    - Integration Tests - หลาย Unit รวมกัน หรือจะรวมกับ DB / API
    - Functional ทดสอบระดับ Component ส่วนของ UI
    - E2E ทดสอบระดับ Flow แต่ยังใช้ Tools มาช่วย
    - Manual - คนมากดเอง ควรมีน้อยที่สุดดดดดดด แพง
  • REST API Apply
    - Unit Tests - มีเยอะๆ
    - Intergration Tests (API)
    - Contract Tests - ตกลง Pattern Request / Response ว่าจะ JSON แบบไหน เอาจริงๆ แบ่ง Micro Service คุยเยอะ ไม่คุยแล้ว ใคร Deploy ก่อนแตกกกกกกก
    - Performance
    - Post-production Test - Test หลัง Deploy
    Note: ถ้าเป็นส่วนอื่นๆ แบบ Mobile เอาตรงนี้ไป Apply ได้เหมือนกัน

เหมือนในบทเรียนจะพยายามทำให้ได้เจ้า Test Pyramid แต่ได้คำตอบแล้วพวก REST API มอง Test Pyramid ยังไง ตอนแรกมองสุดแค่ Integration Test เพราะไม่มี UI 55555

แนวทางทำให้ได้ Test Pyramid

  • Shift Test ลงมาจากให้มาเป็น Unit Tests มีหลายท่า แต่ที่แน่ๆ Refactor ก่อน
  • งาน Test ไม่ใช่ของ QA ช่วยกันทำในแต่ละชั้นให้ดีที่สุด //น่าจะยากเรื่องคน เคยเจอ ผบห เถียงกันว่า Bug ของใคร QA / DEV ไปฟังก็อีหยังงงงง
  • Test เยอะ ไม่ได้บอกว่าดีนะ เอาเทคนิคต่างๆมาตัด Case ลดลง

ผมได้ยินอีกอัน Test Trophy คล้ายกับ Test Pyramid ชั้นที่หนาจะเป็นตัว Integration แทน Unit Test

Testing Manifesto

  • Testing throughout OVER Testing at the end - ทำ Test เรื่อยๆ ไม่ต้องรอตอนท้าย
  • Preventing bugs OVER Finding bugs - Review / TDD / Pair มาช่วยได้
  • Testing Understanding OVER Checking Functionality
  • Build the best System OVER Breaking the System
  • Team Responsibility for quality OVER Tester Responsibility - Test เป็นของทุกคน

4 Go Testing Trick

Struct - ไม่ได้ยินคำนี้มานานาก ปกติใช้ POJO Class / DTO มา โดยเจ้า struct เอาข้อมูลที่มีโครงสร้าง

  • Declaration - Go น่าจะเอาไช้แทน Class เลย
//Declare 
type testCasePrice struct {
	name string
	age  int
	want float64
}
  • Using
//With Label
TC1 := testCasePrice{name: "T Should return 0 for age 0",age: 0, want: 0.0}

//Without Label
TC2 := testCasePrice{"T Should return 0 for age 1", 1, 0.0}

อารมณ์เหมือนย้อนเวลาไปเขียนพวกตระกูล C แต่มันไม่ต้องวุ่นวานเรื่อง memory มั้ง ?

Array

  • Declare & using
testCases := []testCasePrice{TC1, TC2}
-Testable

Testable = DataRow ใน C# ตอนแรกๆ บ่นๆว่าไม่มี OK ถอนคำพูด 5555 เอา Struct มาช่วยนี้เองงง เรียกว่าลดทั้ง Code และเวลาการ Review ด้วย แต่มันจะเหมือนกันหลายๆภาษา Testable /DataRow มีนไม่เหมือนกับ Object ที่ซับซ้อน มันจะ mock ยาก

TestPrice ที่แปลงมาเป็น TestPriceWithTestable

- Coverage

Coverage = บอกว่าอะไรที่ยังไม่ Test ยังไม่เข้าถึง

  • Generate
go test -cover
  • Create Coverage Report เหมือนชาวบ้านและ
#Create Raw Coverage Data พวก dotnet จะเป็น .trx
go test -cover -coverprofile=c.out 
#Convert Raw Coverage Data  to HTML report
go tool cover -html=c.out -o coverage.html
- Black box Testing
  • ยอมให้สร้าง package ลงท้าย _test
  • เราเอา package มา test ในมมมองคนนอก

<package>.<function> ถ้าตั้งชื่้อไม่ดี มันจะแปลกๆ
ticket.TickerPrice >> ticket ซ้ำกัน มันอ่านซ้ำกันจริงๆ เหมือนโดนกับทุกภาษา เรื่อง
- Internal - Read OK
- External - ซ้ำกัน

- Setup/Teardown

รูปแบบ จะเขียนแบบ Function Closure - Return เป็น Function

  • Setup - Pre-Condition เช่น DB
  • Teardown - Post-Condition เช่น ลบข้อมูล
- Static Code Analysis
go fmt  : formats go code
go vet : reports suspicious constructs 
(https://staticcheck.io/docs/getting-started/)
golint : reports poor coding styl

static check

go install -v honnef.co/go/tools/cmd/staticcheck@latest
staticcheck ./

5 Go Document

อธิบาย Code ของเราด้วยการสร้าง Test แหละ โดย Class นั้นต้องช

Example ต่างกับ Test มันจะตรวจเป็น text compare ที่เหลือเขียนเหมือน Test

  • Create Test File
    - example_test.go 
    - example_{FUNCTIONALITY_NAME}_test.go
  • Test Method Pattern
func Example() { ... }
func ExampleF() { ... }   //F = Function
func ExampleT() { ... }   //T = Type
func ExampleT_M() { ... } //T = Type, M = Method

มีหลายอันเดิม suffix ต่อท้ายไป
  • Code Documentation ดูตามรูปใน Slide Show

Gen Doc ได้ >> Code Doc learn-go/SoftwareQuality/sum at main · pingkunga/learn-go (github.com)

go install golang.org/x/tools/cmd/godoc@latest

--Start Server + Allow Firewall
godoc -http=:9095

Ref: Testable Examples in Go - The Go Programming Language

6. Test Http Server

จาก Struct เดิม มันทำเหมือน DTO แล้ว

  • Map JSON ได้ด้วย ใส่ json ต่อไป
type Response struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
	Info string `json:"info"`
}
  • Compare Struct ใช้ DeepEqual ของ reflect
if reflect.DeepEqual(a, b) {
    //YOUR_LOGIC
}

ถ้าเราต้อง Mock ผลลัพธ์จาก API ปกติ ทำยังไง

แยก handler ได้

func HandlerMyAPI(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusOK)
	w.Write([]byte(`{"id": 1, "name": "PingkungA", "age": 33, "info": "dotnet dev/ blogger"}`))
}

func TestMakeHttp(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(HandlerMyAPI))
        //Your Logic
}

Note: ioutil deprecated ย้ายไปตามนี้ For Go, ioutil.ReadAll / ioutil.ReadFile / ioutil.ReadDir deprecated - Stack Overflow

7. Test Double

แนวทางที่ช่วยทำให้ Test มัน Isolated แยกขาดจากกัน ตัดพวก Dependency ออกไปล

  • Dummies -เติมข้อมูลให้ครบ แต่ไม่ได้ใช้ ทำให้ Flow ได้
  • Stubs - บอกไปเลย ถ้าเรียกแล้วให้คืนค่าไป เหมือนตอนเรียน ป โท มีอีก Keyword Drivers
  • Spies - เอาไปดูบางอย่าง เช่น จำนวน Call / ถูกเรียกไหม
  • Fakes - สำหรับพวก 3rd party เอามายัดให้ครบ แล้ว By Pass มันไปเลย
  • Mocks - ทำทุกอย่าง (Dummies / Stubs / Spies / Fakes)+ Self Validate //ฟังอันนี้แล้ว พวก Lib ที่เราใช้กันมันหุ้มหมด

เอาจริงๆ เราเรียกทุกอย่างว่า mock 555555 ไม่รู้ว่า lib dotnet มัน .mock หรือป่าว ตัว moq เลยใช้แบบนั้น

Dependency Injection

  • มันช่วยให้ New Object ได้ง่าย ถ้าในภาษาอื่น dotnet (กลุ่ม AddSingleton , AddTransient และ AddScoped) / spring (@Autowire)
  • ของ Go ถ้าเขียน Signature ตรงกันยัดได้เลย + ใช้ Interface จัดยัดให้เลย //ไม่ต้องบอกอะไรเลย เหมือนจับเอง Code ด้านล่าง GitHub Co-Pilot เค้าบอกมานะ
//Old
package main

import "fmt"

// EmailService is a type that can send messages.
type EmailService struct{}

func (e *EmailService) Send(message string) string {
    return "Email Message Sent: " + message
}

// Greeter is a type that depends on an EmailService.
type Greeter struct {
    MessageService *EmailService
}

func (g *Greeter) Greet() {
    msg := g.MessageService.Send("Hello, World!")
    fmt.Println(msg)
}

func main() {
    greeter := &Greeter{MessageService: &EmailService{}}
    greeter.Greet()
}
//With DI
package main

import "fmt"

// MessageService is a simple interface for types that can send messages.
type MessageService interface {
    Send(message string) string
}

// EmailService is a type that implements the MessageService interface.
type EmailService struct{}

func (e *EmailService) Send(message string) string {
    return "Email Message Sent: " + message
}

// Greeter is a type that depends on a MessageService.
type Greeter struct {
    MessageService MessageService
}

func (g *Greeter) Greet() {
    msg := g.MessageService.Send("Hello, World!")
    fmt.Println(msg)
}

func main() {
    emailService := &EmailService{}
    greeter := &Greeter{MessageService: emailService}
    greeter.Greet()
}

ใน Go มี Lib ช่วยทำ Test Double แบบพวก

ตัว Go เองก็๋มี uber-go/mock: GoMock is a mocking framework for the Go programming language. (github.com)

ว่าจะเขียน Blog Test Double แยกอยู่เหมือนกัน เพราะตัว Test Double มี Draft ไว้ตอนปี 2015

8. Test library

  • stretchr/testify มี Assert เหมือนชาวบ้านแล้ว และทำอย่างอื่นได้ เช่น mock
  • matryer/is อีกเจ้านึง ใช้ is แทน Assert และทำ Color Full Shell

9. Build Tag

เลือกไฟล์ไป Build ตาม Label ที่กำหนดไว้จากหัวไฟล์.go

  • Declare
    - simple ดู tags
    - logic AND (&&) / OR ( || ) / NOT ( ! )
//go:build integration || db
package blabla
//go:build integration && !db
package blablb
  • Using ทำได้อย่าง เอา Label มาบอกจะทำอะไรไหน เลือก Platform / Feature / Test
#For Build Feature 
go build -tags=planA 
go build -tags=planB

#For Build Platform
go build -tags=amd64    

#For Test
go test -tags=integration
go test -v -tags integration
go test -v -tags integration,db

สรุป ในภาษาอื่น เช่น dotnet ตัว go build -tags เอามาทำ

  • การจัดกลุ่ม Test โดยมี Attribute ประมาณนี้ Test Category / Traits
  • เลือก Build ตาม Platform/Feature ใน dotnet เหมือนต้องใช้พวก pre-directive

Ref: What's the difference between `//go:build` and `// +build` directives? - Stack Overflow

10. F.I.R.S.T Principle

  • Fast - ทำงานได้เร็ว คุ้นถ้าจำไม่ผิด ผมเองได้ยนมาว่า CI ทั้ง Flow ไม่ควรจะเกิด 10-15 นาที แบบรวม Test นะ
  • Isolated - Test แต่ละตัว มันขึ้นต่อกันและกัน เช่น ลำดับ เปลี่ยน ยังต้องทำงานได้เหมือนเดิม
  • Repeatable- เอา Test ไป Run ที่ไหน ผลลัพธ์ เหมือนเดิม ไว้ว่าบน Dev หรือ CI/CD
  • Self-validating
  • Timely - เขียน Test ตามหลัก TDD แต่อันนี้ผมว่าจริงๆ ผมว่าทำเถอะ ไม่ต้อง TDD เขียนไว้ตั้งแต่ตั้ง Code แรกๆ เท่าที่ผมเองสังเกตมา ทำ Test แล้ว Method มันจะเล็กลง คนมันขี้เกียจเขียน Test ยาก

Timely อันนี้ผมเองเจอนิยามแปลกๆ "Tests should be written in the correct time" แต่พี่ที่บริษัทจะสอนว่าเขียนตอนเวลาว่าง ความจริง จบ Project ไปแยกย้ายหมด สรุปไม่มี ต้องโวยวายระดับนึงถึงจะยอมให้เขียนกัน 555 บีบแค่ไหนมีสักอัน 2 อัน ยังดี

Reference


Discover more from naiwaen@DebuggingSoft

Subscribe to get the latest posts sent to your email.