golang的单测, 有一些约定, 例如文件名是 xxx.go, 测试文件名必须是 xxx_test.go, 且测试函数的方法名 都是以 Test
开头, 使用go test
命令, 有时发现mock不住,一般都是内联(简短)函数mock失败,可以执行的时候加上编译条件禁止内联 -gcflags=all=-l
.
Feature:
- support a patch for a function
- support a patch for a public member method
- support a patch for a private member method
- support a patch for a interface
- support a patch for a function variable
- support a patch for a global variable
- support patches of a specified sequence for a function
- support patches of a specified sequence for a member method
- support patches of a specified sequence for a interface
- support patches of a specified sequence for a function variable
gomonkey 使用
gomonkey 可以为函数打桩,做数据mock, 有如下功能:
- 为函数打桩
- 为成员方法打桩
- 为全局变量打桩
- 为函数变量打桩
- 为函数打一个特定的桩序列
- 为成员方法打一个特定桩序列
- 为函数变量打一个特定的桩序列
1. 为函数打桩
// @param target: 被mock的函数
// @param double: 桩函数定义
// @return Patches: 测试完成后,通过patches调用 Reset() 删除桩
func ApplyFunc(target, double interface{}) *Patches {
return create().ApplyFunc(target, double)
}
Demo: 使用 gomonkey.ApplyFunc mock netWorkFunc
函数
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,函数一定要有栈分配变量,不然mock不住
return 0, fmt.Errorf("%v", errmsg)
}
return a + b, nil
}
func TestDemo(t *testing.T) {
patches := gomonkey.ApplyFunc(netWorkFunc, func(a, b int) (int, error) {
return 99999999, nil
})
defer patches.Reset()
// out: 99999999
fmt.Println(logicFunc(12, 34))
}
2. 为成员方法打桩
method 与 Func 不同, method 属于类型的一部分, Func 属于包的一部分, 在函数地址分配的方式有所不同,因此不能直接去 ApplyFunc
去 mock, 需要使用 ApplyMethod
// @param target 被mock类型
// @param methodName 方法名
// @param double 桩函数定义
// @return
func ApplyMethod(target reflect.Type, methodName string, double interface{}) *Patches {
return create().ApplyMethod(target, methodName, double)
}
- 无法为 unexported 方法打桩
- 类型为 T的 method只包含recever为 T 的method, 类型为
*T
的包含receiver 为 T |*T
的
- 类型为 T的 method只包含recever为 T 的method, 类型为
- 写桩函数定义时,要把receiver写进去.
Demo:
type MyType struct {
}
func (mt *MyType) logicFunc(a, b int) (int, error) {
sum, err := mt.NetWorkFunc(a, b)
if err != nil {
return 0, err
}
return sum, nil
}
func (mt *MyType) NetWorkFunc(a, b int) (int, error) {
if a < 0 && b < 0 {
errmsg := "a<0 && b<0" //gomonkey有bug,函数一定要有栈分配变量,不然mock不住
return 0, fmt.Errorf("%v", errmsg)
}
return a + b, nil
}
// test case
func Test_ApplyMethod(t *testing.T) {
var m *MyType
patches := gomonkey.ApplyMethod(reflect.TypeOf(m), "NetWorkFunc", func(_ *MyType, a, b int) (int, error) {
return 99999999, nil
})
defer patches.Reset()
res, err := m.logicFunc(12, 34)
assert.Equal(t, err, nil)
assert.Equal(t, res, 99999999)
}
3. 为全局变量打桩
函数签名:
// @param target : 全局变量地址
// @param double : 全局变量的桩
// @retrun Patches
func ApplyGlobalVar(target, double interface{}) *Patches {
return create().ApplyGlobalVar(target, double)
}
var (
num = 100
mat map[string]string
)
func Test_ApplyGlobalVar(t *testing.T) {
patches := gomonkey.ApplyGlobalVar(&num, 150)
defer patches.Reset()
assert.Equal(t, num, 150)
patches2 := gomonkey.ApplyGlobalVar(&mat, map[string]string{"a": "b"})
defer patches2.Reset()
fmt.Println(mat)
}
4. 为函数变量打桩
// @param target 目标函数变量地址
// @param double 目标函数的桩
// @return patches
func ApplyFuncVar(target, double interface{}) *Patches {
return create().ApplyFuncVar(target, double)
}
Demo: mock 一个函数变量的行为
var Marshal = func(v interface{}) ([]byte, error) {
return nil, nil
}
func Test_ApplyFuncVar(t *testing.T) {
str := "hello world."
gomonkey.ApplyFuncVar(&Marshal, func(v interface{}) ([]byte, error) {
return []byte(str), nil
})
bytes, _ := Marshal(nil)
assert.Equal(t, string(bytes), str)
}
5. 为函数打一个特定的桩序列
有时,我们需要多次调用同一个函数,且需要有不同的返回值, 且保持顺序, 如是果是你要怎么实实现?
// @param target 目标函数
// @param outputs 返回值序列
// @return patches
func ApplyFuncSeq(target interface{}, outputs []OutputCell) *Patches {
return create().ApplyFuncSeq(target, outputs)
}
Demo:
func ReadLeaf(value string) (string, error) {
return fmt.Sprintf("%s:%s", "hello", "world"), nil
}
func TestApplyFuncSeq(t *testing.T) {
info1 := "hello world."
info2 := "majm."
info3 := "nancy."
// default time is 1
outputCells := []gomonkey.OutputCell{
{Values: gomonkey.Params{info1, nil}},
{Values: gomonkey.Params{info2, nil}},
{Values: gomonkey.Params{info3, nil}},
}
patches := gomonkey.ApplyFuncSeq(ReadLeaf, outputCells)
defer patches.Reset()
result1, err := ReadLeaf("1")
assert.Equal(t, info1, result1)
assert.Equal(t, err, nil)
result2, err := ReadLeaf("2")
assert.Equal(t, info2, result2)
assert.Equal(t, err, nil)
result3, err := ReadLeaf("3")
assert.Equal(t, info3, result3)
assert.Equal(t, err, nil)
}
type Params []interface{}
type OutputCell struct {
Values Params
Times int
}
Params: []interface{}: 返回结果
Times: 出现的次数
- 如果 outputs + times 声明了 N次,当调用超过N次后悔报错.
- Times 默认是 1 次
6. 为成员方法打一个特定桩序列
// @param target 目标类型
// @param methodName 目标类型方法
// @param outputs 返回值序列
// @return patches
func ApplyMethodSeq(target reflect.Type, methodName string, outputs []OutputCell) *Patches {
return create().ApplyMethodSeq(target, methodName, outputs)
}
Demo:
type Etcd struct {
}
func (e *Etcd) Retrieve(url string) (string, error) {
output := fmt.Sprintf("%s, %s!", "Hello", "Etcd")
return output, nil
}
func Test_ApplyMethodSeq(t *testing.T) {
info1 := "hello world."
info2 := "majm."
info3 := "nancy."
var e *Etcd
outputs := []gomonkey.OutputCell{
{Values: gomonkey.Params{info1, nil}},
{Values: gomonkey.Params{info2, nil}},
{Values: gomonkey.Params{info3, nil}},
}
patches := gomonkey.ApplyMethodSeq(reflect.TypeOf(e), "Retrieve", outputs)
defer patches.Reset()
result1, err := e.Retrieve("url1")
assert.Equal(t, info1, result1)
assert.Equal(t, err, nil)
result2, err := e.Retrieve("url2")
assert.Equal(t, info2, result2)
assert.Equal(t, err, nil)
result3, err := e.Retrieve("url3")
assert.Equal(t, info3, result3)
assert.Equal(t, err, nil)
}
7. 为函数变量打一个特定的桩序列
// @param: target 目标函数变量地址
// @param: outputs 返回结果序列
// @return: patches
func ApplyFuncVarSeq(target interface{}, outputs []OutputCell) *Patches {
return create().ApplyFuncVarSeq(target, outputs)
}
Demo:
func Test_ApplyFuncVarSeq(t *testing.T) {
info1 := []byte("hello world.")
info2 := []byte("majm.")
info3 := []byte("nancy.")
outputCells := []gomonkey.OutputCell{
{Values: gomonkey.Params{info1, nil}},
{Values: gomonkey.Params{info2, nil}},
{Values: gomonkey.Params{info3, nil}},
}
patches := gomonkey.ApplyFuncVarSeq(&Marshal, outputCells)
defer patches.Reset()
result1, err := Marshal("1")
assert.Equal(t, result1, info1)
assert.Equal(t, err, nil)
result2, err := Marshal("2")
assert.Equal(t, result2, info2)
assert.Equal(t, err, nil)
result3, err := Marshal("3")
assert.Equal(t, result3, info3)
assert.Equal(t, err, nil)
}