一、單元測試是什么
單元測試是用來對一個模塊、一個函數或者一個類來進行正確性檢驗的測試工作。
二、單元測試的意義
1.提高代碼質量:編寫測試用例會迫使開發人員仔細思考代碼的設計和必須完成的工作,進而會改善代碼質量。
2.簡化調試過程:開發人員可以測試單個模塊的功能,不依賴外部工程和數據源。
3.保證重構正確:單元測試可以有效地測試某個程序模塊的行為,是未來重構代碼的信心保證。
4.盡早發現問題:單元測試由功能對應的開發者完成,測試考慮的范圍更加全面,可以盡早發現大部分問題。
三、Golang單元測試框架
3.1 Golang內置testing包
3.1.1 簡單的測試
簡單測試用例定義如下:
func TestXXXX(t *testing.T) {
// ...
}
- Go 語言推薦測試文件和源代碼文件放在一塊,測試文件以 _test.go 結尾。
- 函數名必須以 Test 開頭,后面一般跟待測試的函數名
- 參數為 t *testing.T
3.1.2 Benchmark 基准測試
基准測試用例的定義如下:
func BenchmarkName(b *testing.B){
// ...
}
- 函數名必須以 Benchmark 開頭,后面一般跟待測試的函數名
- 參數為 b *testing.B。
3.1.3 運行測試用例
# 匹配當前目錄下*_test.go命令的文件,執行每一個測試函數
go test -v
# 執行 calc_test.go 文件中的所有測試函數
go test -v calc_test.go calc.go
# 指定特定的測試函數(其中:-count=1用於跳過緩存)
go test -v -run TestAdd calc_test.go calc.go -count=1
# 執行基准測試
go test -benchmem -bench .
3.1.4 簡單的測試示例
當前 package 有 calc.go 一個文件,我們想測試 calc.go 中的 Add 和 Substract 函數,那么應該新建 calc_test.go 作為測試文件。
testing/
| -- calc.go
| -- calc_test.go
calc.go
的代碼如下:
package testing
func Add(a, b int) int {
return a + b
}
func Substract(a, b int) int {
return a - b
}
calc_test.go
的代碼如下
package testing
import (
"testing"
)
func TestAdd(t *testing.T) {
a := 2
b := 3
ret := Add(a, b)
if ret != a+b {
t.Fatalf("Expect:%d Actual:%d", a+b, ret)
}
}
func TestSubtests(t *testing.T) {
// 嵌套
t.Run("TestSubstract", func(t *testing.T) {
a := 3
b := 2
ret := Substract(a, b)
if ret != a-b {
t.Fatalf("Expect:%d Actual:%d", a-b, ret)
}
})
}
3.2 GoConvey測試框架
GoConvey 是個相當不錯的 Go 測試工具,支持 go test。可直接在終端窗口和瀏覽器上使用。
3.2.1.安裝依賴:
go get github.com/smartystreets/goconvey
3.2.2.測試用例
package/
| --calc.go
| --calc_test.go
calc.go
的代碼如下:
package goconvey
import "errors"
func Add(a, b int) int {
return a + b
}
func Subtract(a, b int) int {
return a - b
}
func Multiply(a, b int) int {
return a * b
}
func Division(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("被除數不能為 0")
}
return a / b, nil
}
calc_test.go
的代碼如下:
package goconvey
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func Test_Add(t *testing.T) {
Convey("將兩數相加", t, func() {
So(Add(1, 2), ShouldEqual, 3)
})
}
func Test_Substract(t *testing.T) {
// 忽略斷言
SkipConvey("將兩數相減", t, func() {
So(Subtract(2, 1), ShouldEqual, 1)
})
}
func Test_Multiply(t *testing.T) {
Convey("將兩數相乘", t, func() {
So(Multiply(3, 2), ShouldEqual, 6)
})
}
func Test_Division(t *testing.T) {
Convey("將兩數相除", t, func() {
// 嵌套
Convey("除以非 0 數", func() {
num, err := Division(10, 2)
So(err, ShouldBeNil)
So(num, ShouldEqual, 5)
// 忽略斷言
SkipSo(num, ShouldNotBeNil)
})
Convey("除以 0", func() {
_, err := Division(10, 0)
So(err, ShouldNotBeNil)
})
})
}
3.2.3. 運行測試
1.終端窗口使用:go test -v
2.Web瀏覽器使用:在相應的目錄執行 goconvey,然后訪問http://localhost:8080
3.3 testify測試框架
3.3.1. 安裝依賴
go get github.com/stretchr/testify
3.3.2 測試用例
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_assert(t *testing.T) {
a := 2
b := 3
assert.Equal(t, a+b, 5, "They should be equal")
}
require包提供與assert包相同的全局函數,相比assert沒有返回值,而是終止當前測試
import (
"testing"
"github.com/stretchr/testify/require"
)
func Test_require(t *testing.T) {
var name = "dazuo"
var age = 24
require.Equal(t, "dazuo", name, "they should be equal")
require.Equal(t, 24, age, "they should be equal")
}
mock Package
提供了一種輕松編寫模擬對象的機制,可以在編寫測試代碼時代替實際對象使用模擬對象。
package testify
import (
"fmt"
"testing"
"github.com/stretchr/testify/mock"
)
type Storage interface {
Store(key, value string) (int, error)
Load(key string) (string, error)
}
// 測試用例,當真實對象不可用時,使用mock對象代替
type mockStorage struct {
mock.Mock
}
func (ms *mockStorage) Store(key, value string) (int, error) {
args := ms.Called(key, value)
return args.Int(0), args.Error(1)
}
func (ms *mockStorage) Load(key string) (string, error) {
args := ms.Called(key)
return args.String(0), args.Error(1)
}
func Test_mock(t *testing.T) {
mockS := &mockStorage{}
mockS.On("Store", "name", "dazuo").Return(20, nil).Once()
var storage Storage = mockS
i, e := storage.Store("name", "dazuo")
if e != nil {
panic(e)
}
fmt.Println(i)
}
四、Stub/Mock框架
4.1 GoStub框架
4.1.1.安裝依賴
go get github.com/prashantv/gostub
4.1.2.使用場景
- 為全局變量打樁
- 為函數打樁
- 為過程打樁(當一個函數沒有返回值時,該函數我們一般稱為過程)
4.1.3.測試示例
import (
"fmt"
"github.com/prashantv/gostub"
)
// 1.為全局變量打樁
var counter = 100
func stubGlobalVariable() {
stubs := gostub.Stub(&counter, 200)
defer stubs.Reset()
fmt.Println("Counter:", counter)
}
// 2.為函數打樁
var Exec = func() {
fmt.Println("Exec")
}
func stubFunc() {
stubs := gostub.Stub(&Exec, func() {
fmt.Println("Stub Exec")
})
Exec()
defer stubs.Reset()
}
// 3.為過程打樁(當一個函數沒有返回值時,該函數我們一般稱為過程。很多時候,我們將資源清理類函數定義為過程。)
var CleanUp = cleanUp
func cleanUp(val string) {
fmt.Println(val)
}
func stubPath() {
stubs := gostub.StubFunc(&CleanUp)
defer stubs.Reset()
CleanUp("Hello go")
}
func main() {
//stubGlobalVariable()
//stubFunc()
stubPath()
}
4.2 GoMock框架
4.2.1 GoMock簡介
gomock 是官方提供的 mock 框架,同時還提供了 mockgen 工具用來輔助生成測試代碼。
使用如下命令即可安裝:
go get -u github.com/golang/mock/gomock
go get -u github.com/golang/mock/mockgen
4.2.2 測試示例
1.新建db.go
文件,代碼如下:
// db.go
type DB interface {
Get(key string) (int, error)
}
func GetFromDB(db DB, key string) int {
if value, err := db.Get(key); err != nil {
return value
}
return -1
}
2.使用 mockgen 生成 db_mock.go
mockgen -source=db.go -destination=db_mock.go -package=PACKAGE_NAME
3.寫測試用例 TestGetFromDB
import (
"errors"
"testing"
"github.com/golang/mock/gomock"
)
func TestGetFromDB(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish() // 斷言 DB.Get() 方法是否被調用
m := NewMockDB(ctrl)
// 打樁
m.EXPECT().Get(gomock.Eq("Tom")).Return(0, errors.New("not exist"))
if v := GetFromDB(m, "Tom"); v != -1 {
t.Fatal("expected -1, but got", v)
}
}
4.3 gomonkey框架
4.3.1 安裝依賴
go get github.com/agiledragon/gomonkey
4.3.2 測試用例
example_test.go
的代碼如下:
import (
"fmt"
"testing"
"github.com/agiledragon/gomonkey"
"github.com/smartystreets/goconvey/convey"
)
// 假設networkFunc是一個網絡調用
func networkFunc(a, b int) int {
return a + b
}
// 本地單測一般不會進行網絡調用,所以要mock住networkFunc
func Test_MockNetworkFunc(t *testing.T) {
convey.Convey("123", t, func() {
p := gomonkey.NewPatches()
defer p.Reset()
p.ApplyFunc(networkFunc, func(a, b int) int {
fmt.Println("in mock function")
return a + b
})
_ = networkFunc(10, 20)
})
}
4.3.3 運行測試
如果啟用了內聯,將無法mock,可以通過添加命令行參數:
- go1.10以下的:-gcflags=-l
- go1.10以上的:-gcflags=all=-l
go test -v example_test.go -gcflags=all=-l