相信大家都知道專案內不導入測試,未來越來越多功能,技術債就會越來越多,接手的人罵聲連連,而寫測試的簡單與否決定專案初期是否要先導入。為什麼專案要導入測試,導入測試有什麼好處,對於團隊而言,導入測試好處實在太多了,底下列了幾點是我個人覺得非常重要的。
- 減少 Review 時間
- 降低修改程式碼產生的的錯誤
- 確保程式碼品質
內建 testing 套件
在 Go 語言內,不需要而外安裝任何第三方套件就可以開使寫測試,首先該將測試放在哪個目錄內呢?不需要建立特定目錄來存放測試程式碼,而是針對每個 Go 的原始檔案,建立一個全新測試檔案,並且檔名最後加上_test
就可以了,假設程式碼為 car.go
那麼測試程式就是 car_test.go
,底下舉個範例
package car import "errors" // Car struct type Car struct { Name string Price float32 } // SetName set car name func (c *Car) SetName(name string) string { if name != "" { c.Name = name } return c.Name } // New Object func New(name string, price float32) (*Car, error) { if name == "" { return nil, errors.New("missing name") } return &Car{ Name: name, Price: price, }, nil }驗證上面的程式碼可以建立
car_test.go
,並且寫下第一個測試程式:
// Simple testing what different between Fatal and Error func TestNew(t *testing.T) { c, err := New("", 100) if err != nil { t.Fatal("got errors:", err) } if c == nil { t.Error("car should be nil") } }首先 func 名稱一定要以
Test
作為開頭,而 Go 內建 testing 套件,可以使用簡易的 t.Fatal 或 t.Error 來驗證錯誤,這兩個的差異在於 t.Fatal 會中斷測試,而 t.Error 不會,簡單來說,假設您需要整個完整測試後才顯示錯誤,那就需要用 t.Error,反之就使用 t.Fatal 來中斷測試。
使用 testify 套件
這邊只會介紹一個第三方套件那就是 testify,裡面內建很多好用的測試等大家發掘,底下用簡單的 assert 套件來修改上方的測試程式:func TestNewWithAssert(t *testing.T) { c, err := New("", 100) assert.NotNil(t, err) assert.Error(t, err) assert.Nil(t, c) c, err = New("foo", 100) assert.Nil(t, err) assert.NoError(t, err) assert.NotNil(t, c) assert.Equal(t, "foo", c.Name) }有沒有看起來比較簡潔。這邊測試用的 command,也可以針對單一函式做測試。
$ go test -v -run=TestNewWithAssert ./example18-write-testing-and-doc/...可以看到
-run
讓開發者可以針對單一函式做測試,對於大型專案來說非常方便,假設修正完 bug,並且寫了測試,就可以針對單一函式做測試,這點 Go 做得相當棒。
平行測試
講平行測試之前,跟大家講個用 vscode 編輯器寫測試的一個小技巧,就是透過 vscode 可以幫忙產生測試程式碼,該如何使用呢?可以先將要測試的函式全選,然後按下command
+ shift
+ p
,就會出現底下命令列選擇。
這邊為什麼要平行測試呢?原因是單一函式測試,假設一個情境需要執行時間為 0.5 秒,那麼假設寫了 10 種狀況,就需要 10 * 0.5 秒,這樣花費太久了。這時候就需要請 Go 幫忙做平行測試。先看看底下範例:
func TestCar_SetName(t *testing.T) { type fields struct { Name string Price float32 } type args struct { name string } tests := []struct { name string fields fields args args want string }{ { name: "no input name", fields: fields{ Name: "foo", Price: 100, }, args: args{ name: "", }, want: "foo", }, { name: "input name", fields: fields{ Name: "foo", Price: 100, }, args: args{ name: "bar", }, want: "bar", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c := &Car{ Name: tt.fields.Name, Price: tt.fields.Price, } if got := c.SetName(tt.args.name); got != tt.want { t.Errorf("Car.SetName() = %v, want %v", got, tt.want) } }) } }上面範例跑了兩個測試,一個是沒有 input value,一個則是有 input,根據 for 迴圈會依序執行測試,其中裡面的
t.Run
是指 sub test,如下圖
上述的程式碼都是 vscode 幫忙產生的,開發者只需要把測試資料補上就可以了。假設有 10 個情境需要測試,那該如何讓 Go 幫忙平行測試呢?請使用 t.Parallel()
for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() c := &Car{ Name: tt.fields.Name, Price: tt.fields.Price, } if got := c.SetName(tt.args.name); got != tt.want { t.Errorf("Car.SetName() = %v, want %v", got, tt.want) } }) }在
t.Run
的 callback 測試內補上 t.Parallel()
就可以了喔。寫到這邊,大家應該可以看出一個問題,就是平行測試的內容怎麼都會是測試同一個情境,也就是本來要測試 10 種情境,但是會發現 Go 把最後一個情境同時跑了 10 次?這邊的問題點出在哪邊,請大家注意 tt
變數,由於跑平行測試,那麼 for 迴圈最後一次就會蓋掉之前的所有 tt 變數,要修正此狀況也非常容易,在迴圈內重新宣告一次即可 tt := tt
for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { t.Parallel() c := &Car{ Name: tt.fields.Name, Price: tt.fields.Price, } if got := c.SetName(tt.args.name); got != tt.want { t.Errorf("Car.SetName() = %v, want %v", got, tt.want) } }) }