引言
gomonkey 是笔者开源的一款 Go 语言 的打桩框架,目标是让用户在单元测试中低成本的完成打桩,从而将精力聚焦于业务功能的开发。gomonkey 接口友好,功能强大,目前已被很多项目使用,用户遍及世界多个国家。
近一年,在诸多用户的共同努力下,gomonkey 社区发展的很快,连续发布了 8 个版本,不仅优化了一些基础特性,而且还新增了很多扩展特性,非常实用接地气。与此同时,gomonkey 的 star 数从 0.5k 跃升到了 1.1k,受到了国内外 gopher 的广泛赞赏和肯定。
gomonkey 新增或优化的主要特性汇总:
特性 | 分类 | 贡献者 | 备注 |
---|---|---|---|
全面支持 arm64 架构 | 新增 | hengwu0 | PR55 PR58 |
全面支持为 private method 打桩了 | 新增 | hengwu0 lockdown56 |
PR65 PR67 PR85 |
全面支持 386 架构 | 新增 | segdumping | PR75 |
支持为 method 打桩时不传入receiver | 优化 | AVOlili | PR78 |
支持为 func/func var/method 打桩时直接指定返回值 | 新增 | AVOlili | PR78 |
支持为 method 打桩时不必转化为reflect.Type类型,同时兼容原有的用法 | 优化 | AVOlili | PR83 |
支持为 method 打桩不传入receiver时函数可为变参 | 优化 | punchio | PR90 |
感谢所有 gomonkey 的贡献者,每一个特性都凝结着大家的心血和汗水。虽然我们不曾见过,但彼此心往一处想,劲往一处使,共同推动 gomonkey 社区持续发展,不断繁荣,从一个胜利走向另一个胜利。
在众多新特性中,gomonkey 全面支持 arm64 架构
是对业界影响最大的一个特性。去年笔者刚发布支持该特性的版本后,就很意外的收到了 Bouk 大神的来信:
这里需要强调一下:Bouke 是 Go 语言 monkey工程的创建者,在 2015 年就发表了 Go 语言猴子补丁原理的文章。毫无疑问,gomonkey) 的思维底座主要来自 Bouke 的贡献,向他致敬,非常感谢!
如果你对 gomonkey 全面支持 arm64 架构感兴趣,可以进一步阅读笔者之前写的一篇文章《gomonkey 全面支持 arm64 了》。
gomonkey 惯用法刷新
gomonkey 基础特性列表如下:
- 支持为一个函数打一个桩
- 支持为一个成员方法打一个桩
- 支持为一个全局变量打一个桩
- 支持为一个函数变量打一个桩
- 支持为一个函数打一个特定的桩序列
- 支持为一个成员方法打一个特定的桩序列
- 支持为一个函数变量打一个特定的桩序列
想要了解 gomonkey 的这些基础特性,可以参考几年前笔者的一篇文章《gomonkey 1.0 正式发布》。
interface 惯用法刷新
之前很多 gopher 习惯使用 GoMock 框架对 interface 进行打桩,笔者当时也写了一篇文章《GoMock框架使用指南》。后来有一些 gomonkey 用户想用 gomonkey 对 interface 进行打桩,从而减少多个打桩框架的学习成本和测试用例的维护成本。
刷新1:当为 interface 打一个桩时,用户直接复用组合之前的 ApplyFunc 和 ApplyMethod 接口即可
对 interface 打一个桩,其实不用提供类似 ApplyInterface 的接口,而仅仅是让用户复用组合之前的 ApplyFunc 和 ApplyMethod 接口。原因其实很简单,当我们定义了一个 interface 时,系统中就会有一个或多个实现类(struct),我们可以通过 ApplyFunc 接口让 interface 变量指向一个实现类对象,然后通过 ApplyMethod 接口来改变该实现类的行为,这就相当于对 interface 完成了打桩。
示例代码:先构造一个 Etcd 对象 e,通过第一层 convey 调用 ApplyFunc 让 Db 的 interface 变量指向 e,然后在第二层 convey 中调用 ApplyMethod 对 Db 完成打一个桩。
func TestApplyInterfaceReused(t *testing.T) {
e := &fake.Etcd{}
Convey("TestApplyInterface", t, func() {
patches := ApplyFunc(fake.NewDb, func(_ string) fake.Db {
return e
})
defer patches.Reset()
db := fake.NewDb("mysql")
Convey("TestApplyInterface", func() {
info := "hello interface"
patches.ApplyMethod(e, "Retrieve",
func(_ *fake.Etcd, _ string) (string, error) {
return info, nil
})
output, err := db.Retrieve("")
So(err, ShouldEqual, nil)
So(output, ShouldEqual, info)
})
})
}
刷新2:当为 interface 打一个桩序列时,用户直接复用组合之前的 ApplyFunc 和 ApplyMethodSeq 接口即可
同理,为 interface 打一个桩序列,也不用提供提供类似 ApplyInterfaceSeq 的接口。
示例代码:先构造一个 Etcd 对象 e,通过第一层 convey 调用 ApplyFunc 让 Db 的 interface 变量指向 e,然后在第二层 convey 中调用 ApplyMethodSeq 对 interface Db 完成打一个桩,在第一个第二层 convey 中调用 ApplyMethodSeq 对 Db 完成打一个特定的桩序列。
func TestApplyInterfaceReused(t *testing.T) {
e := &fake.Etcd{}
Convey("TestApplyInterface", t, func() {
patches := ApplyFunc(fake.NewDb, func(_ string) fake.Db {
return e
})
defer patches.Reset()
db := fake.NewDb("mysql")
Convey("TestApplyInterfaceSeq", func() {
info1 := "hello cpp"
info2 := "hello golang"
info3 := "hello gomonkey"
outputs := []OutputCell{
{Values: Params{info1, nil}},
{Values: Params{info2, nil}},
{Values: Params{info3, nil}},
}
patches.ApplyMethodSeq(e, "Retrieve", outputs)
output, err := db.Retrieve("")
So(err, ShouldEqual, nil)
So(output, ShouldEqual, info1)
output, err = db.Retrieve("")
So(err, ShouldEqual, nil)
So(output, ShouldEqual, info2)
output, err = db.Retrieve("")
So(err, ShouldEqual, nil)
So(output, ShouldEqual, info3)
})
})
}
method 惯用法刷新
先回顾一下 method 打桩的原有方式。
示例如下:reflect.TypeOf 的参数是一个指针类型,而 NewSlice 返回的仅仅是一个 Slice 引用类型,所以仍需再定义一个变量 s。
func TestApplyMethod(t *testing.T) {
slice := fake.NewSlice()
var s *fake.Slice
Convey("TestApplyMethod", t, func() {
Convey("for succ", func() {
err := slice.Add(1)
So(err, ShouldEqual, nil)
patches := ApplyMethod(reflect.TypeOf(s), "Add", func(_ *fake.Slice, _ int) error {
return nil
})
defer patches.Reset()
err = slice.Add(1)
So(err, ShouldEqual, nil)
err = slice.Remove(1)
So(err, ShouldEqual, nil)
So(len(slice), ShouldEqual, 0)
})
})
}
刷新3:当为 method 打桩时可以不传入 reflect.TypeOf 类型参数了
示例代码:ApplyMethod 第一个参数以前传 reflect.TypeOf(s),现在仅需传 s,同时兼容原有的用例,就是说新用例可以使用 s 代替 reflect.TypeOf(s),而老用例可以保持 reflect.TypeOf(s) 不变。
func TestApplyMethod(t *testing.T) {
slice := fake.NewSlice()
var s *fake.Slice
Convey("TestApplyMethod", t, func() {
Convey("for succ", func() {
err := slice.Add(1)
So(err, ShouldEqual, nil)
patches := ApplyMethod(reflect.TypeOf(s), "Add", func(_ *fake.Slice, _ int) error {
return nil
})
defer patches.Reset()
err = slice.Add(1)
So(err, ShouldEqual, nil)
err = slice.Remove(1)
So(err, ShouldEqual, nil)
So(len(slice), ShouldEqual, 0)
})
})
}
刷新4:当为 method 打桩时可以不传入 receiver 参数了
要使用该特性,就不能再使用 ApplyMethod 接口了,而是使用 ApplyMethodFunc 接口。
示例代码:比上面 TestApplyMethod 示例代码 ApplyMethod 的第三个函数参数 func(_ *fake.Slice, _ int) error 少了第一个子参数 *fake.Slice,而简化成 func(_ int) error。
func TestApplyMethodFunc(t *testing.T) {
slice := fake.NewSlice()
var s *fake.Slice
Convey("TestApplyMethodFunc", t, func() {
Convey("for succ", func() {
err := slice.Add(1)
So(err, ShouldEqual, nil)
patches := ApplyMethodFunc(s, "Add", func(_ int) error {
return nil
})
defer patches.Reset()
err = slice.Add(1)
So(err, ShouldEqual, nil)
err = slice.Remove(1)
So(err, ShouldEqual, nil)
So(len(slice), ShouldEqual, 0)
})
})
}
刷新5:当为 method 打桩时可以直接指定返回值
要使用该特性,就不能再使用 ApplyMethod 接口了,而是使用 ApplyMethodReturn 接口。
示例代码:ApplyMethodReturn 接口从第三个参数开始就是桩的返回值。
func TestApplyMethodReturn(t *testing.T) {
e := &fake.Etcd{}
Convey("TestApplyMethodReturn", t, func() {
Convey("declares the values to be returned", func() {
info := "hello cpp"
patches := ApplyMethodReturn(e, "Retrieve", info, nil)
defer patches.Reset()
for i := 0; i < 10; i++ {
output, err := e.Retrieve("")
So(err, ShouldEqual, nil)
So(output, ShouldEqual, info)
}
})
})
}
刷新6:当 method 为私有时,也可以完成打桩
在 Go 语言中,通过标志符首字母的大小写来控制可见性。当标志符首字母为大写时,标志符可导出,包外可见,否则仅在包内可见,不可导出。
之前对 method 打桩时,method 必须可导出,否则在反射接口中会查询失败,从而导致打桩失败,抛出异常:
panic("retrieve method by name failed")
后来很多 gomonkey 用户反馈,private method 打桩的价值也很大,我们就自研了定制的反射包 creflect,而穿越 reflect 包的限制,成功支持了 private method。一些想使用 private method 特性的用户,可能会误使用 ApplyMethod 接口,导致错误,而提供该特性的扩展接口是 ApplyPrivateMethod。
示例代码:有了 ApplyPrivateMethod 接口后,可以跨包给私有方法打桩,第二层有两个 convey,说明有两个用例,第一个用例针对 private pointer method,第二个用例针对 private value method。
func TestApplyPrivateMethod(t *testing.T) {
Convey("TestApplyPrivateMethod", t, func() {
Convey("patch private pointer method in the different package", func() {
f := new(fake.PrivateMethodStruct)
var s *fake.PrivateMethodStruct
patches := ApplyPrivateMethod(s, "ok", func(_ *fake.PrivateMethodStruct) bool {
return false
})
defer patches.Reset()
result := f.Happy()
So(result, ShouldEqual, "unhappy")
})
Convey("patch private value method in the different package", func() {
s := fake.PrivateMethodStruct{}
patches := ApplyPrivateMethod(s, "haveEaten", func(_ fake.PrivateMethodStruct) bool {
return false
})
defer patches.Reset()
result := s.AreYouHungry()
So(result, ShouldEqual, "I am hungry")
})
})
}
如果你想进一步了解 private method 特性,请阅读笔者之前写的一篇文章《gomonkey支持为private method打桩了》。
func 惯用法刷新
刷新7:当为 func 打桩时可以直接指定返回值
要使用该特性,就不能再使用 ApplyFunc 接口了,而是使用 ApplyFuncReturn 接口。
示例代码:ApplyFuncReturn 接口从第二个参数开始就是桩的返回值。
func TestApplyFuncReturn(t *testing.T) {
Convey("TestApplyFuncReturn", t, func() {
Convey("declares the values to be returned", func() {
info := "hello cpp"
patches := ApplyFuncReturn(fake.ReadLeaf, info, nil)
defer patches.Reset()
for i := 0; i < 10; i++ {
output, err := fake.ReadLeaf("")
So(err, ShouldEqual, nil)
So(output, ShouldEqual, info)
}
})
})
}
func var 惯用法刷新
刷新8:当为 func var 打桩时可以直接指定返回值
要使用该特性,就不能再使用 ApplyFuncVar 接口了,而是使用 ApplyFuncVarReturn 接口。
示例代码:ApplyFuncVarReturn 接口从第二个参数开始就是桩的返回值。
func TestApplyFuncVarReturn(t *testing.T) {
Convey("TestApplyFuncVarReturn", t, func() {
Convey("declares the values to be returned", func() {
info := "hello cpp"
patches := ApplyFuncVarReturn(&fake.Marshal, []byte(info), nil)
defer patches.Reset()
for i := 0; i < 10; i++ {
bytes, err := fake.Marshal("")
So(err, ShouldEqual, nil)
So(string(bytes), ShouldEqual, info)
}
})
})
}
constructor 惯用法刷新
很多时候,我们先使用 Apply 族函数接口完成一个目标对象的打桩,它返回一个 patches 对象,然后我们再使用 Apply 族方法接口完成其他目标对象的打桩。
示例代码:测试用例中需要对两个函数 (fake.Exec 和 json.Unmarshal) 都进行打桩,我们分别调用 ApplyFunc 接口完成打桩。
func TestIndependent(t *testing.T) {
Convey("TestIndependent", t, func() {
Convey("two funcs", func() {
patches := ApplyFunc(fake.Exec, func(_ string, _ ...string) (string, error) {
return outputExpect, nil
})
defer patches.Reset()
patches.ApplyFunc(json.Unmarshal, func(data []byte, v interface{}) error {
p := v.(*map[int]int)
*p = make(map[int]int)
(*p)[1] = 2
(*p)[2] = 4
return nil
})
output, err := fake.Exec("", "")
So(err, ShouldEqual, nil)
So(output, ShouldEqual, outputExpect)
var m map[int]int
err = json.Unmarshal(nil, &m)
So(err, ShouldEqual, nil)
So(m[1], ShouldEqual, 2)
So(m[2], ShouldEqual, 4)
})
})
}
刷新9:当打桩接口统一时可以批处理
我们先构造一个 patches 对象,然后通过批处理完成打桩。
示例代码:
func TestBatch(t *testing.T) {
Convey("TestBatch", t, func() {
Convey("two funcs", func() {
patchPairs := [][2]interface{}{
{
fake.Exec,
func(_ string, _ ...string) (string, error) {
return outputExpect, nil
},
},
{
json.Unmarshal,
func(_ []byte, v interface{}) error {
p := v.(*map[int]int)
*p = make(map[int]int)
(*p)[1] = 2
(*p)[2] = 4
return nil
},
},
}
patches := NewPatches()
defer patches.Reset()
for _, pair := range patchPairs {
patches.ApplyFunc(pair[0], pair[1])
}
output, err := fake.Exec("", "")
So(err, ShouldEqual, nil)
So(output, ShouldEqual, outputExpect)
var m map[int]int
err = json.Unmarshal(nil, &m)
So(err, ShouldEqual, nil)
So(m[1], ShouldEqual, 2)
So(m[2], ShouldEqual, 4)
})
})
}
刷新10:当打桩操作可复用时封装 fake 关键字
常见的 fake 关键字包括 DB,HTTP,AMQP 和 K8S 等,可以通过 DDD 的六边形架构来完整识别。还有一些 fake 关键字,对应标准库函数操作,比如 随机数 RandInt。
我们封装 fake 关键子时,如果需要打桩,那么需要将 patches 对象传入。
示例代码:通过 FakeRandInt 函数实现了 fake 关键字 RandInt,将 gomonkey 的打桩接口封装起来,非常通用,可以在所有与随机数打桩相关的用例中复用。
func FakeRandInt(patches *Patches, randomNumbers []int) {
var outputs []OutputCell
for _, rn := range randomNumbers {
outputs = append(outputs, OutputCell{Values: Params{rn}})
}
patches.ApplyFuncSeq(rand.Intn, outputs)
}
示例代码:对于 fake 关键字 RandInt 的使用,用户不需要关注 gomonkey 特性的具体使用方法,仅仅注入 patches 对象和随机数切片就可以完成随机数生成的通用打桩。
func TestGenerateAnswerByOnce(t *testing.T) {
Convey("Given the system random number is 1964", t, func() {
patches := NewPatches()
FakeRandInt(patches, []int{1964})
defer patches.Reset()
Convey("When generate answer", func() {
answer := generateAnswer()
Convey("Then the answer is 1964", func() {
So(answer, ShouldEqual, "1964")
})
})
})
}
func TestGenerateAnswerBySeveralTimes(t *testing.T) {
Convey("Given the system random number seq is [788, 2260]", t, func() {
patches := NewPatches()
FakeRandInt(patches, []int{788, 2260})
defer patches.Reset()
Convey("When generate answer", func() {
answer := generateAnswer()
Convey("Then the answer is 7826", func() {
So(answer, ShouldEqual, "7826")
})
})
})
}
小结
这一年, gomonkey 社区快速发展,使得 Go 语言打桩工作变得越来越美好,受到了国内外 gopher 的广泛赞赏和肯定。
为了让更多的 gopher 低成本受益,笔者特意总结了 gomonkey 惯用法的十大刷新,希望读者可以快速掌握,并能及时将学到的技能应用到开发者测试的具体实践中去,使得测试用例的开发效率和表达力都进一步得到提升。