Golang單元測試
目錄
1、單元測試概述
1.1 什么是單元&單元測試
1.2 為什么進行單元測試
1.3 單元測試用例編寫的原則
1.4 單測用例規(guī)定
2、golang 常用的單測框架
2.1 testing
2.2 goconvey
2.3 testify

1、單元測試概述
1.1 什么是單元&單元測試
單元是應(yīng)用的最小可測試部件,如函數(shù)和對象的方法 單元測試是軟件開發(fā)中對最小單位進行正確性檢驗的測試工作
1.2 為什么進行單元測試
保證變更/重構(gòu)的正確性,特別是在一些頻繁變動和多人合作開發(fā)的項目中 簡化調(diào)試過程:可以輕松的讓我們知道哪一部分代碼出了問題 單測最好的文檔:在單測中直接給出具體接口的使用方法,是最好的實例代碼
1.3 單元測試用例編寫的原則
單一原則:一個測試用例只負責一個場景 原子性:結(jié)果只有兩種情況: Pass、Fail優(yōu)先要核心組件和邏輯的測試用例 高頻使用庫, util,重點覆蓋
1.4 單測用例規(guī)定
文件名必須要 xx_test.go命名測試方法必須是 TestXXX開頭方法中的參數(shù)必須是 t *testing.T測試文件和被測試文件必須在一個包中
2、golang 常用的單測框架
2.1 testing
https://golang.google.cn/pkg/testing/
2.1.1 單元測試
Go提供了test工具用于代碼的單元測試,test工具會查找包下以_test.go結(jié)尾的文件,調(diào)用測試文件中以 Test或Benchmark開頭的函數(shù)并給出運行結(jié)果
測試函數(shù)需要導(dǎo)入testing包,并定義以Test開頭的函數(shù),參數(shù)為testing.T指針類型,在測試函數(shù)中調(diào)用函數(shù)進行返回值測試,當測試失敗可通過testing.T結(jié)構(gòu)體的Error函數(shù)拋出錯誤

單元測試是對某個功能的測試 命令行執(zhí)行
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函數(shù)參數(shù):%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函數(shù)參數(shù):%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函數(shù)參數(shù):%d %d, 期望: %d, 實際: %d", a, b, want, actual)
}
}
執(zhí)行測試
? pwd
golang-learning/chapter06/pkg03
? go test -v .
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
=== RUN TestMul
compute_test.go:21: Mul函數(shù)參數(shù):10 20, 期望: 300, 實際: 200
--- FAIL: TestMul (0.00s)
=== RUN TestDiv
compute_test.go:31: Div函數(shù)參數(shù):10 20, 期望: 2, 實際: 0
--- FAIL: TestDiv (0.00s)
FAIL
FAIL pkg03 0.198s
FAIL
只執(zhí)行某個函數(shù)
go test -run=TestAdd -v .
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok pkg03 0.706s
正則過濾函數(shù)名
go test -run=TestM.* -v .
2.1.2 測試覆蓋率
用于統(tǒng)計目標包有百分之多少的代碼參與了單測
使用go test工具進行單元測試并將測試覆蓋率覆蓋分析結(jié)果輸出到cover.out文件
例如上面的例子
go test -v -cover
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
=== RUN TestMul
compute_test.go:21: Mul函數(shù)參數(shù):10 20, 期望: 300, 實際: 200
--- FAIL: TestMul (0.00s)
=== RUN TestDiv
compute_test.go:31: Div函數(shù)參數(shù):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

分析測試結(jié)果,打開測試覆蓋率結(jié)果文件,查看測試覆蓋率
go tool cover -html cover.out
2.1.3 子測試 t.run
func TestMul2(t *testing.T) {
t.Run("正數(shù)", func(t *testing.T) {
if Mul(4, 5) != 20 {
t.Fatal("muli.zhengshu.error")
}
})
t.Run("負數(shù)", func(t *testing.T) {
if Mul(2, -3) != -6 {
t.Fatal("muli.fushu.error")
}
})
}
執(zhí)行測試
? go test -v .
=== RUN TestMul2
=== RUN TestMul2/正數(shù)
=== RUN TestMul2/負數(shù)
--- PASS: TestMul2 (0.00s)
--- PASS: TestMul2/正數(shù) (0.00s)
--- PASS: TestMul2/負數(shù) (0.00s)
指定func/sub運行子測試
? go test -run=TestMul2/正數(shù) -v
=== RUN TestMul2
=== RUN TestMul2/正數(shù)
--- PASS: TestMul2 (0.00s)
--- PASS: TestMul2/正數(shù) (0.00s)
PASS
ok pkg03 0.675s
子測試的作用:table-driven tests
所有用例的數(shù)據(jù)組織在切片
cases中,看起來就像一張表,借助循環(huán)創(chuàng)建子測試。這樣寫的好處有新增用例非常簡單,只需給 cases新增一條測試數(shù)據(jù)即可測試代碼可讀性好,直觀地能夠看到每個子測試的參數(shù)和期待的返回值 用例失敗時,報錯信息的格式比較統(tǒng)一,測試報告易于閱讀 如果數(shù)據(jù)量較大,或是一些二進制數(shù)據(jù),推薦使用相對路徑從文件中讀取 舉例:prometheus 源碼[1]
2.2 goconvey
goconvey是一個第三方測試框架,其最大好處就是對常規(guī)的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("不設(shè)置分數(shù)可能出錯", t, func() {
sc, err := stu.GetAvgScore()
Convey("獲取分數(shù)出錯了", func() {
So(err, ShouldBeError)
})
Convey("分數(shù)為0", func() {
So(sc, ShouldEqual, 0)
})
})
Convey("正常情況", t, func() {
stu.ChiScore = 60
stu.EngScore = 70
stu.MathScore = 80
score, err := stu.GetAvgScore()
Convey("獲取分數(shù)出錯了", func() {
So(err, ShouldBeNil)
})
Convey("平均分大于60", func() {
So(score, ShouldBeGreaterThan, 60)
})
})
}
執(zhí)行go test -v .
? go test -v .
=== RUN TestNewStudent
start test new
空的name初始化錯誤 ?
stu對象為nil ?
2 total assertions
--- PASS: TestNewStudent (0.00s)
=== RUN TestScore
不設(shè)置分數(shù)可能出錯
獲取分數(shù)出錯了 ?
分數(shù)為0 ?
4 total assertions
正常情況
獲取分數(shù)出錯了 ?
平均分大于60 ?
6 total assertions
--- PASS: TestScore (0.00s)
PASS
ok pkg04 0.126s
2.2.2 圖形化使用
確保本地有 goconvey的二進制
go get github.com/smartystreets/goconvey
# 會將對應(yīng)的二進制文件放到 $GOPATH/bin 下面
編輯環(huán)境變量把 GOPATH/bin加入PATH里面 或者寫全路徑到測試的目錄下,執(zhí)行 goconvey,啟動http 8000,自動運行測試用例瀏覽器訪問 http://127.0.0.1:8000
最終效果如下

2.3 testify
2.3.1 簡單使用
業(yè)務(wù)代碼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")
}
執(zhí)行測試
? go test -v .
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok pkg05 1.216s
2.3.2 表驅(qū)動測試
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隔離第三方依賴或者復(fù)雜調(diào)用testfiy/mock使得偽造對象的輸入輸出值可以在運行時決定參考:https://github.com/euclidr/testingo
2.3.4 單元測試覆蓋率應(yīng)用實例
https://github.com/m3db/m3/pull/3525

參考資料
prometheus 源碼:https://github.com/prometheus/prometheus/blob/main/web/api/v1/api_test.go: https://github.com/prometheus/prometheus/blob/main/web/api/v1/api_test.go
