跳至主要内容

[Note] Golang Test 測試筆記

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 開頭作為測試函式的名稱,例如,TestNewDeckTest_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.Fatalt.Error 這兩個方法,t.Fatal 會讓程式終止,t.Error 則不會。

使用 Table Tests

如果每增加一個測試案例,就要多一組 if { t.Errorf(...) } 是很沒效率也不好維護的做法,因此,如果這個測試是會需要帶入多個不同的 test cases 時,可以

  1. 先以 slice of struct 訂好一系列的變數和預期的結果
  2. 透過迴圈的方式重複執行
使用 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)測試

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

⚠️ 特別留意 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)
}

參考