0%

Go-Test-Gomonkey使用

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 使用

github

gomonkey 可以为函数打桩,做数据mock, 有如下功能:

  1. 为函数打桩
  2. 为成员方法打桩
  3. 为全局变量打桩
  4. 为函数变量打桩
  5. 为函数打一个特定的桩序列
  6. 为成员方法打一个特定桩序列
  7. 为函数变量打一个特定的桩序列

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)
}
    1. 无法为 unexported 方法打桩
    1. 类型为 T的 method只包含recever为 T 的method, 类型为*T 的包含receiver 为 T | *T
    1. 写桩函数定义时,要把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)
}

欢迎关注我的其它发布渠道