Golang單元測試
-
-
- 1.1 什麼是單元&單元測試
- 1.2 為什麼進行單元測試
- 1.3 單元測試用例編寫的原則
- 2、golang 常用的單測框架
-

1、單元測試概述
1.1 什麼是單元&單元測試
- 單元是應用的最小可測試部件,如函式和物件的方法
- 單元測試是軟體開發中對最小單位進行正確性檢驗的測試工作
1.2 為什麼進行單元測試
- 保證變更/重構的正確性,特別是在一些頻繁變動和多人合作開發的專案中
- 簡化除錯過程: 可以輕鬆的讓我們知道哪一部分程式碼出了問題
- 單測最好的文件:在單測中直接給出具體介面的使用方法,是最好的例項程式碼
1.3 單元測試用例編寫的原則
- 單一原則:一個測試用例只負責一個場景
- 原子性:結果只有兩種情況:
Pass
、Fail
- 優先要核心元件和邏輯的測試用例
- 高頻使用庫,
util
,重點覆蓋
1.4 單測用例規定
xx_test.go TestXXX t *testing.T
2、golang 常用的單測框架
2.1 testing
https://golang.google.cn/pkg/testing/
2.1.1 單元測試
Go
提供了 test
工具用於程式碼的單元測試, test
工具會查詢包下以 _test.go
結尾的檔案,呼叫測試檔案中以 Test
或 Benchmark
開頭的函式並給出執行結果
測試函式需要匯入 testing
包,並定義以 Test
開頭的函式,引數為 testing.T
指標型別,在測試函式中呼叫函式進行返回值測試,當測試失敗可通過 testing.T
結構體的 Error
函式丟擲錯誤

單元測試是對某個功能的測試
命令列執行
go test 包名 # 測試整個包 go test -v . go test 包名/檔名 # 測試某個檔案
簡單使用
準備待測程式碼 compute.go
package pkg03 func Add(a, b int) int { return a + b } func Mul(a, b int) int { return a * b } func Div(a, b int) int { return a / b }
準備測試用例 compute_test.go
package pkg03 import "testing" func TestAdd(t *testing.T) { a := 10 b := 20 want := 30 actual := Add(a, b) if want != actual { t.Errorf("Add函式引數:%d %d, 期望: %d, 實際: %d", a, b, want, actual) } } func TestMul(t *testing.T) { a := 10 b := 20 want := 300 actual := Mul(a, b) if want != actual { t.Errorf("Mul函式引數:%d %d, 期望: %d, 實際: %d", a, b, want, actual) } } func TestDiv(t *testing.T) { a := 10 b := 20 want := 2 actual := Div(a, b) if want != actual { t.Errorf("Div函式引數:%d %d, 期望: %d, 實際: %d", a, b, want, actual) } }
執行測試
➜ pwd golang-learning/chapter06/pkg03 ➜ go test -v . === RUN TestAdd --- PASS: TestAdd (0.00s) === RUN TestMul compute_test.go:21: Mul函式引數:10 20, 期望: 300, 實際: 200 --- FAIL: TestMul (0.00s) === RUN TestDiv compute_test.go:31: Div函式引數:10 20, 期望: 2, 實際: 0 --- FAIL: TestDiv (0.00s) FAIL FAIL pkg03 0.198s FAIL
只執行某個函式
go test -run=TestAdd -v . === RUN TestAdd --- PASS: TestAdd (0.00s) PASS ok pkg03 0.706s
正則過濾函式名
go test -run=TestM.* -v .
2.1.2 測試覆蓋率
用於統計目標包有百分之多少的程式碼參與了單測
使用 go test
工具進行單元測試並將測試覆蓋率覆蓋分析結果輸出到 cover.out
檔案
例如上面的例子
go test -v -cover === RUN TestAdd --- PASS: TestAdd (0.00s) === RUN TestMul compute_test.go:21: Mul函式引數:10 20, 期望: 300, 實際: 200 --- FAIL: TestMul (0.00s) === RUN TestDiv compute_test.go:31: Div函式引數:10 20, 期望: 2, 實際: 0 --- FAIL: TestDiv (0.00s) FAIL coverage: 100.0% of statements exit status 1 FAIL pkg03 0.185s
生成測試覆蓋率檔案
go test -v -coverprofile=cover.out === RUN TestAdd --- PASS: TestAdd (0.00s) === RUN TestAddFlag --- PASS: TestAddFlag (0.00s) PASS coverage: 75.0% of statements ok testcalc/calc 0.960s

分析測試結果,開啟測試覆蓋率結果檔案,檢視測試覆蓋率
go tool cover -html cover.out
2.1.3 子測試t.run
func TestMul2(t *testing.T) { t.Run("正數", func(t *testing.T) { if Mul(4, 5) != 20 { t.Fatal("muli.zhengshu.error") } }) t.Run("負數", func(t *testing.T) { if Mul(2, -3) != -6 { t.Fatal("muli.fushu.error") } }) }
執行測試
➜ go test -v . === RUN TestMul2 === RUN TestMul2/正數 === RUN TestMul2/負數 --- PASS: TestMul2 (0.00s) --- PASS: TestMul2/正數 (0.00s) --- PASS: TestMul2/負數 (0.00s)
指定 func/sub
執行子測試
➜ go test -run=TestMul2/正數 -v === RUN TestMul2 === RUN TestMul2/正數 --- PASS: TestMul2 (0.00s) --- PASS: TestMul2/正數 (0.00s) PASS ok pkg03 0.675s
子測試的作用: table-driven tests
-
所有用例的資料組織在切片
cases
中,看起來就像一張表,藉助迴圈建立子測試。這樣寫的好處有cases
-
舉例: prometheus原始碼:https://github.com/prometheus/prometheus/blob/main/web/api/v1/api_test.go
2.2 goconvey
goconvey
是一個第三方測試框架,其最大好處就是對常規的 if else
進行了高度封裝
2.2.1 基本使用
準備待測程式碼 student.go
package pkg04 import "fmt" type Student struct { Name string ChiScore int EngScore int MathScore int } func NewStudent(name string) (*Student, error) { if name == "" { return nil, fmt.Errorf("name為空") } return &Student{ Name: name, }, nil } func (s *Student) GetAvgScore() (int, error) { score := s.ChiScore + s.EngScore + s.MathScore if score == 0 { return 0, fmt.Errorf("全都是0分") } return score / 3, nil }
參考官方示例,準備測試用例 student_test.go
直觀來講,使用 goconvey
的好處是不用再寫多個 if
判斷
package pkg04 import ( . "github.com/smartystreets/goconvey/convey" "testing" ) func TestNewStudent(t *testing.T) { Convey("start test new", t, func() { stu, err := NewStudent("") Convey("空的name初始化錯誤", func() { So(err, ShouldBeError) }) Convey("stu物件為nil", func() { So(stu, ShouldBeNil) }) }) } func TestScore(t *testing.T) { stu, _ := NewStudent("hh") Convey("不設定分數可能出錯", t, func() { sc, err := stu.GetAvgScore() Convey("獲取分數出錯了", func() { So(err, ShouldBeError) }) Convey("分數為0", func() { So(sc, ShouldEqual, 0) }) }) Convey("正常情況", t, func() { stu.ChiScore = 60 stu.EngScore = 70 stu.MathScore = 80 score, err := stu.GetAvgScore() Convey("獲取分數出錯了", func() { So(err, ShouldBeNil) }) Convey("平均分大於60", func() { So(score, ShouldBeGreaterThan, 60) }) }) }
執行 go test -v .
➜ go test -v . === RUN TestNewStudent start test new 空的name初始化錯誤 ✔ stu物件為nil ✔ 2 total assertions --- PASS: TestNewStudent (0.00s) === RUN TestScore 不設定分數可能出錯 獲取分數出錯了 ✔ 分數為0 ✔ 4 total assertions 正常情況 獲取分數出錯了 ✔ 平均分大於60 ✔ 6 total assertions --- PASS: TestScore (0.00s) PASS ok pkg04 0.126s
2.2.2 圖形化使用
- 確保本地有
goconvey
的二進位制
go get github.com/smartystreets/goconvey # 會將對應的二進位制檔案放到 $GOPATH/bin 下面
- 編輯環境變數把
GOPATH/bin
加入PATH
裡面 或者寫全路徑 - 到測試的目錄下,執行
goconvey
,啟動http 8000
,自動執行測試用例 - 瀏覽器訪問 http://127.0.0.1:8000
最終效果如下

2.3 testify
2.3.1 簡單使用
業務程式碼 cal.go
package pkg05 func Add(x int ) (result int) { result = x + 2 return result }
測試用例 cal_test.go
package pkg05 import ( "github.com/stretchr/testify/assert" "testing" ) func TestAdd(t *testing.T) { // assert equality assert.Equal(t, Add(5), 7, "they should be equal") }
執行測試
➜ go test -v . === RUN TestAdd --- PASS: TestAdd (0.00s) PASS ok pkg05 1.216s
2.3.2 表驅動測試
package pkg05 import ( "github.com/stretchr/testify/assert" "testing" ) func TestAdd(t *testing.T) { // assert equality assert.Equal(t, Add(5), 7, "they should be equal") } func TestCal(t *testing.T) { ass := assert.New(t) var tests = []struct { input int expected int }{ {2, 4}, {-1, 1}, {0, 2}, {-5, -3}, {999999997, 999999999}, } for _, test := range tests { ass.Equal(Add(test.input), test.expected) } }
2.3.3 mock功能
testify/mock testfiy/mock
2.3.4 單元測試覆蓋率應用例項
https://github.com/m3db/m3/pull/3525

- Gradle打包工具入門
- 服務網格和Istio初識-續
- 服務網格和Istio初識
- Golang與非對稱加密
- ack叢集Terway網路場景下的vSwitch擴容
- Golang與對稱加密
- 基於ack k8s叢集排程的方案設計
- 基於Dockerfile構建容器映象的最佳實踐
- Golang反射-下篇
- Golang反射-上篇
- Azure DevOps的使用入門
- Golang介面型別-下篇
- Golang介面型別-上篇
- 基於Python實現原生的登入驗證碼
- Golang開發命令列工具之flag包的使用
- Golang檔案操作-下篇
- k8s環境下處理容器時間問題的多種姿勢
- Golang基準測試
- 淺談Prometheus的資料儲存
- Golang單元測試