这篇文章介绍了Golang的单元测试工具Gomonkey。Gomonkey支持为函数、成员方法、函数变量、接口和全局变量打桩。文章详细展示了如何使用Gomonkey进行单元测试,并列出了可能导致打桩失败的原因。同时,文章还解释了什么是内联,并展示了如何禁用内联进行测试。
功能列表
- 支持为一个函数打一个桩
- 支持为一个函数打一个特定的桩序列
- 支持为一个成员方法打一个桩
- 支持为一个成员方法打一个特定的桩序列
- 支持为一个函数变量打一个桩
- 支持为一个函数变量打一个特定的桩序列
- 支持为一个接口打桩
- 支持为一个接口打一个特定的桩序列
- 支持为一个全局变量打一个桩
打桩失败的可能原因
gomonkey
不是并发安全的。如果有多协程并发对同一个目标的打桩的情况,则需要将之前的协程先优雅退出。- 打桩目标为内联的函数或成员方法。可通过命令行参数
-gcflags=-l
(go1.10 版本之前)或-gcflags=all=-l
(go1.10 版本及之后)关闭内联优化。 gomonkey
对于私有成员方法的打桩失败。go1.6
版本的反射机制支持私有成员方法的查询,而go1.7
及之后的版本却不支持,所以当用户使用go1.7
及之后的版本时,gomonkey
对于私有成员方法的打桩会触发异常。
示例
package monkey
import (
"fmt"
"reflect"
"testing"
"github.com/agiledragon/gomonkey"
. "github.com/smartystreets/goconvey/convey"
)
// ###############################################################
// ########################## ApplyFunc ##########################
// ###############################################################
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 TestApplyFunc(t *testing.T) {
Convey("Test ApplyFunc: ", t, func() {
// 为函数打桩
var patches = gomonkey.ApplyFunc(netWorkFunc, func(a, b int) (int, error) {
return 20, nil
})
defer patches.Reset()
sum, err := logicFunc(10, 20) // logicFunc中调用了netWorkFunc
So(sum, ShouldEqual, 20)
So(err, ShouldBeNil)
})
}
// ###############################################################
// ######################### ApplyMethod #########################
// ###############################################################
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 TestApplyMethod(t *testing.T) {
Convey("Test ApplyMethod: ", t, func() {
var p *myType
patches := gomonkey.ApplyMethod(reflect.TypeOf(p), "NetWorkFunc", func(_ *myType, a, b int) (int, error) {
return 20, nil
})
defer patches.Reset()
var m myType
sum, err := m.logicFunc(10, 20)
So(sum, ShouldEqual, 20)
So(err, ShouldBeNil)
})
}
// ###############################################################
// ######################### ApplyGlobalVar ######################
// ###############################################################
var num = 10
func TestApplyGlobalVar(t *testing.T) {
Convey("Test ApplyGlobalVar: ", t, func() {
Convey("change", func() {
patches := gomonkey.ApplyGlobalVar(&num, 150)
defer patches.Reset()
So(num, ShouldEqual, 150)
})
Convey("recover", func() {
So(num, ShouldEqual, 10)
})
})
}
// ###############################################################
// ########################## ApplyFuncSeq #######################
// ###############################################################
func getInt() (int) {
a := 100
return a
}
func TestMockFuncSeq(t *testing.T) {
Convey("Test ApplyFuncSeq: ", 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)
})
Convey("Test ApplyFuncSeq: ", t, func () {
So(getInt(), ShouldEqual, 100)
})
}
执行命令
[iarno@nd01v monkey]$ go test -v -gcflags=-l .
=== RUN TestApplyFunc
Test ApplyFunc: ✔✔
2 total assertions
--- PASS: TestApplyFunc (0.00s)
=== RUN TestApplyMethod
Test ApplyMethod: ✔✔
4 total assertions
--- PASS: TestApplyMethod (0.00s)
=== RUN TestApplyGlobalVar
Test ApplyGlobalVar:
change ✔
recover ✔
6 total assertions
--- PASS: TestApplyGlobalVar (0.00s)
=== RUN TestMockFuncSeq
Test ApplyFuncSeq: ✔✔✔✔
10 total assertions
Test ApplyFuncSeq: ✔
11 total assertions
--- PASS: TestMockFuncSeq (0.00s)
PASS
ok hellogolang/monkey (cached)
什么是内联
为了减少函数调用时的堆栈等开销,对于简短的函数,会在编译时,直接内嵌调用的代码。
我们禁用下内联,然后执行, go test -v -gcflags=-l monkey_test.go