[Note] Golang Test 測試筆記
- Testing in Go @ ieftimov
- 如何在 Go 專案內寫測試 @ AppleBoy
CLI
# -v 顯示詳細測試結果(verbose)
# -cover 顯示測試覆蓋率
# -failfast 發生錯誤就停止測試
# -coverprofile 產生測試結果的檔案
# -html 產生測試結果的 HTML 檔案
# -short 跳過有 t.Skip 或 testing.Short() 的函式
# 測試某個 package
$ go test <package-name> -v
$ go test sandbox/go-sandbox/car -v
# 測試專案內的所有檔案
$ go test ./...
# 測試某資料夾內的所有檔案
$ go test ./car/...
# 只測試檔案中的某個 function
$ go test -run=TestCar_SetName -cover -v ./car/...
# 檢視測試覆蓋率
$ go test -cover . # 只顯示在 Terminal
# 檢視測試報告及未被覆蓋到的程式碼
$ go test -coverprofile cover.out ./...
$ go tool cover -html=cover.out -o cover.html
$ open cover.html
# 清除測試的 cache
$ go clean -testcache
# 避免多個 package 的 test 同時執行
$ go test -p 1 ./... # 限制 parallel 的數量為 1
建立測試
建立測試檔
依照慣例,假設我們要測試的是 main.go
- 測試檔會直接放在要被測試的檔案旁邊
- 檔名「必須」為
main_test.go
撰寫測試檔
- 匯入
testing
這個 package - 「必須」以
Test
開頭作為測試函式的名稱,例如,TestNewDeck
或Test_newDeck
- 在測試的函式中,帶入參數
t *testing.T
- 當有非預期的錯誤產生時,使用
t.Errorf()
或t.Fail
來表示測試失敗 - 使用
go test .
執行測試
// deck_test.go
package deck
import (
"testing"
)
func TestNewDeck(t *testing.T) {
d := newDeck()
// 檢驗 d 是否為 16
if len(d) != 16 {
// 錯誤訊息
t.Errorf("Expected deck length of 20, but got %v", len(d))
}
}
t.Errorf 與 t.Fatal
在 go 中內建 testing
package 可以使用,其中 testing.T
型別提供了 t.Fatal
和 t.Error
這兩個方法,t.Fatal
會讓程式終止,t.Error
則不會。
使用 Table Tests
如果每增加一個測試案例,就要多一組 if { t.Errorf(...) }
是很沒效率也不好維護的做法,因此,如果這個測試是會需要帶入多個不同的 test cases 時,可以
- 先以 slice of struct 訂好一系列的變數和預期的結果
- 透過迴圈的方式重複執行
使用 Table Tests
// main_test.go
package main
import (
"testing"
)
func TestIsPrime(t *testing.T) {
// 先將測試案例建立好
primeTests := []struct {
name string
testNum int
expected bool
}{
{"prime", 7, true},
{"not prime", 8, false},
{"zero", 0, false},
{"one", 1, false},
{"negative number", -2, false},
// 新增其他 test cases
}
// 再以迴圈的方式執行所有的測試案例
for _, e := range primeTests {
if result, _ := isPrime(e.testNum); result != e.expected {
t.Errorf("Test failed. The number %v is expected to get %v but get %v", e.testNum, e.expected, result)
}
}
}
Sub Testing
使用 t.Run(name, func(t *testing.T) {/* ... */})
可以執行 sub test:
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("SetName() = %v, want %v", got, tt.want)
}
})
}
使用 testify
testify @ Github
資料庫(Database)測試
- 使用 testify 提供的 suite package
API 測試
測試具有 params 的 API
測試具有 params 的 API 時,需要透過 ctx.Params
把參數傳入:
// 特別留意 params 的寫法
// 假設我們要呼叫的路由是 /api/outsources/5
// 直接寫在路由中會吃不到,要使用 ctx.Params 傳進去
// s.ctx.Params = gin.Params{{Key: "id", Value: "5"}}
func (s *OutsourceSuite) Test_UpdateOutsource_expectNotFound() {
s.ctx.Params = gin.Params{{Key: "id", Value: "5"}}
payload := `{"dataSyncAt": "2020-08-20T09:15:41.567Z", "dataSyncStatus": "OK"}`
s.withJSONData("PATCH", "/api/outsources", payload)
s.OutsourceAPI.UpdateOutsource(s.ctx)
assert.Equal(s.T(), http.StatusNotFound, s.recorder.Code)
}
func (s *OutsourceSuite) withJSONData(method string, endPoint string, payload string) {
s.ctx.Request = httptest.NewRequest(method, endPoint, strings.NewReader(payload))
s.ctx.Request.Header.Set("Content-Type", "application/json")
}
Response
Response Header
- ResponseRecorder @ pkg.go
⚠️ 特別留意
w.WriteHeader()
一定要放在最後面,也就是所有 Header 都 Set 完後才呼叫WriteHeader()
。
func TestUpdateRemainingQuota(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Quota-Limit", "10000")
w.Header().Set("X-Quota-Remaining", "9998")
w.Header().Set("X-Quota-Time-To-Reset", "1603540800000")
w.WriteHeader(http.StatusOK)
}
req := httptest.NewRequest("GET", "http://example.com/foo", nil)
w := httptest.NewRecorder()
handler(w, req)
resp := w.Result()
pcc, err := NewClient(clientID, clientSecret, certPath, keyPath)
assert.NoError(t, err)
assert.NoError(t, pcc.updateRemainingQuota(resp))
expect := &Quota{
Limit: 10000,
Remaining: 9998,
TimeToReset: time.Date(2020, 10, 24, 12, 0, 0, 0, time.UTC),
}
assert.Equal(t, expect, pcc.Quota)
}
參考
- 如何在 Go 專案內寫測試 @ AppleBoy
- How To Avoid Parallel Execution of Tests in Go @ Medium
- Go: The Complete Developer's Guide (Golang) @ Udemy by Stephen Grider