簡介
golang單測,有一些約定蟹略,例如文件名是xxx.go,那么對應(yīng)的測試文件就是xxx_test.go遏佣,單測的函數(shù)都需要是Test開頭挖炬,然后使用go test命令,有時發(fā)現(xiàn)mock不住状婶,一般都是內(nèi)聯(lián)(簡短)函數(shù)mock失敗意敛,可以執(zhí)行的時候加上編譯條件禁止內(nèi)聯(lián) -gcflags=all=-l
1. gomonkey
gomonkey用于mock跑單測,有以下的功能:
- 為函數(shù)打樁
- 為成員方法打樁
- 為全局變量打樁
- 為函數(shù)變量打樁
- 為函數(shù)打一個特定的樁序列
- 為成員方法打一個特定的樁序列
- 為函數(shù)變量打一個特定的樁序列
下面依次說說這幾種方法的使用
1.1 使用
1.1.1 mock函數(shù) ApplyFunc
// @param[in] target 被mock的函數(shù)
// @param[in] double 樁函數(shù)定義
// @retval patches 測試完成后膛虫,通過patches調(diào)用Reset刪除樁
func ApplyFunc(target, double interface{}) *Patches
func (this *Patches) ApplyFunc(target, double interface{}) *Patches
樁函數(shù)的入?yún)⒉菀觥⒎祷刂岛鸵籱ock的函數(shù)保持一致。
舉個例子稍刀,例如現(xiàn)有調(diào)用鏈:logicFunc()-> netWorkFunc()
我們要測試logicFunc撩独,而logicFunc里面調(diào)用了一個netWorkFunc,因為本地單測一般不會進(jìn)行網(wǎng)絡(luò)調(diào)用掉丽,所以我們要mock住netWorkFunc跌榔。
代碼實例:
package main
import (
"fmt"
"testing"
"github.com/agiledragon/gomonkey"
"github.com/smartystreets/goconvey/convey"
)
func logicFunc(a,b int) (int, error) {
sum, err := netWorkFunc(a, b)
if err != nil {
return 0, err
}
return sum, nil
}
func netWorkFunc(a,b int) (int,error){
if a < 0 && b < 0 {
errmsg := "a<0 && b<0" //gomonkey有bug,函數(shù)一定要有棧分配變量捶障,不然mock不住
return 0, fmt.Errorf("%v",errmsg)
}
return a+b, nil
}
func TestMockFunc(t *testing.T) {
convey.Convey("TestMockFunc1", t, func() {
var p1 = gomonkey.ApplyFunc(netWorkFunc, func(a, b int) (int, error) {
fmt.Println("in mock function")
return a+b, nil
})
defer p1.Reset()
sum, err := logicFunc(10, 20)
convey.So(sum, convey.ShouldEqual, 30)
convey.So(err, convey.ShouldBeNil)
})
}
直接用gomonkey.ApplyFunc,來mock netWorkFunc這個函數(shù)纲刀,然后調(diào)用logicFun项炼,再用斷言判斷一致返回值是否符合預(yù)期。
這里用了convey包做斷言示绊,這本包斷言挺豐富的锭部,用起來很方便,也很簡單:
convey.Convey("case的名字", t, func() {
具體測試case
convey.So(...) //斷言
})
1.1.2 mock成員方法 ApplyMethod
method和function不同面褐,實際上是屬于類型的一部分拌禾,不像函數(shù)屬于包的一部分,在函數(shù)地址的分配上會有所不同展哭,因此不能直接用ApplyFunc去mock湃窍,這時就需要使用ApplyMethod了。
// @param[in] target 被mock的類型
// @param[in] methodName 要被mocket的函數(shù)名字,是個string
// @param[in] double 樁函數(shù)定義
// @retval patches 測試完成后匪傍,通過patches調(diào)用Reset刪除樁
func ApplyMethod(target reflect.Type, methodName string, double interface{}) *Patches
func (this *Patches) ApplyMethod(target reflect.Type, methodName string, double interface{}) *Patches
下面的例子和上面ApplyFunc的差不多您市,也是logicFunc()-> netWorkFunc(),只不過從function變成了method役衡,原理就是利用了reflect中的有幾點要注意:
沒辦法mock unexported method茵休。原因可以看reflect的原理。還有人論證為啥你永遠(yuǎn)不改測試unexported method:https://medium.com/@thrawn01/why-you-should-never-test-private-methods-f822358e010
類型T的method只包含receiver是T的;類型*T的method包含receiver是T和*T的榕莺。
寫樁函數(shù)定義時俐芯,要把receiver寫進(jìn)去
例子:
type myType struct {
}
func (m *myType) logicFunc(a,b int) (int, error) {
sum, err := m.NetWorkFunc(a, b)
if err != nil {
return 0, err
}
return sum, nil
}
func (m *myType) NetWorkFunc(a,b int) (int,error){
if a < 0 && b < 0 {
errmsg := "a<0 && b<0"
return 0, fmt.Errorf("%v",errmsg)
}
return a+b, nil
}
func TestMockMethod(t *testing.T) {
Convey("TestMockMethod", t, func() {
var p *myType
fmt.Printf("method num:%d\n", reflect.TypeOf(p).NumMethod())
p1 := gomonkey.ApplyMethod(reflect.TypeOf(p), "NetWorkFunc", func(_ *myType, a,b int) (int,error) {
if a < 0 && b < 0 {
errmsg := "a<0 && b<0"
return 0, fmt.Errorf("%v",errmsg)
}
return a+b, nil
})
defer p1.Reset()
var m myType
sum, err := m.logicFunc(10, 20)
So(sum, ShouldEqual, 30)
So(err, ShouldBeNil)
})
}
1.1.3 mock全局變量 ApplyGlobalVar
// @param[in] target 全局變量的地址
// @param[in] double 全局變量的樁
func ApplyGlobalVar(target, double interface{}) *Patches
func (this *Patches) ApplyGlobalVar(target, double interface{}) *Patches
全局變量的mock很簡單,直接看代碼吧:
var num = 10
func TestApplyGlobalVar(t *testing.T) {
Convey("TestApplyGlobalVar", t, func() {
Convey("change", func() {
patches := ApplyGlobalVar(&num, 150)
defer patches.Reset()
So(num, ShouldEqual, 150)
})
Convey("recover", func() {
So(num, ShouldEqual, 10)
})
})
}
1.1.4 mock函數(shù)變量 ApplyFuncVar
// @param[in] target 函數(shù)變量的地址
// @param[in] double 樁函數(shù)的定義
func ApplyFuncVar(target, double interface{}) *Patches
func (this *Patches) ApplyFuncVar(target, double interface{}) *Patches
這個也很簡單钉鸯,直接看代碼就明白了:
var funcVar = func(a,b int) (int,error) {
if a < 0 && b < 0 {
errmsg := "a<0 && b<0"
return 0, fmt.Errorf("%v",errmsg)
}
return a+b, nil
}
func TestMockFuncVar(t *testing.T) {
Convey("TestMockFuncVar", t, func() {
gomonkey.ApplyFuncVar(&funcVar, func(a,b int)(int,error) {
return a-b, nil
})
v, err := funcVar(20, 5)
So(v, ShouldEqual, 15)
So(err, ShouldBeNil)
})
}
1.1.5 mock函數(shù)序列 ApplyFuncSeq
有一種場景吧史,被mock的函數(shù),可能會被多次調(diào)用亏拉,我們希望按固定的順序扣蜻,然后每次調(diào)用的返回值都不一樣,我們可以用一個全局變量記錄這是第幾次調(diào)用及塘,然后樁函數(shù)里面做判斷莽使,更簡潔的方法,就是用ApplyFuncSeq
type Params []interface{}
type OutputCell struct {
Values Params
Times int
}
// @param[in] target 要被mocket的函數(shù)
// @param[in] outputs 返回值
func ApplyFuncSeq(target interface{}, outputs []OutputCell) *Patches
func (this *Patches) ApplyFuncSeq(target interface{}, outputs []OutputCell) *Patches
其中Values是返回值笙僚,是一個[]interface{}芳肌,對應(yīng)實際可能有多個返回值。
看一下例子:
func getInt() (int) {
a := 1
fmt.Println("not in mock")
return a
}
func TestMockFuncSeq(t *testing.T) {
Convey("func seq", t, func() {
outputs := []gomonkey.OutputCell{
{Values:gomonkey.Params{2}, Times:1},
{Values:gomonkey.Params{1}, Times:0},
{Values:gomonkey.Params{3}, Times:2},
}
var p1 = gomonkey.ApplyFuncSeq(getInt, outputs)
defer p1.Reset()
So(getInt(), ShouldEqual, 2)
So(getInt(), ShouldEqual, 1)
So(getInt(), ShouldEqual, 3)
So(getInt(), ShouldEqual, 3)
})
}
注意:
- 對于Times肋层,默認(rèn)都是1次亿笤,填1次和0次其實都是1次
- 如果總共會調(diào)用N次,實際調(diào)用超過N次栋猖,那么會報錯
1.1.6 mock成員方法序列 ApplyMethodSeq
同樣的净薛,既然有 ApplyFunSeq,那么就有 ApplyMethodSeq蒲拉,基本都是一樣的肃拜,不演示了
1.1.7 mock函數(shù)變量序列 ApplyFuncVarSeq
同樣的,既然有 ApplyFunSeq雌团,那么就有 ApplyFunVarSeq燃领,基本都是一樣的,不演示了