你该刷新gomonkey的惯用法了

我是一只可爱的土拨鼠,专注于分享 Go 职场、招聘和求职,解 Gopher 之忧!欢迎关注我。

欢迎大家加入Go招聘交流群,来这里找志同道合的小伙伴!跟土拨鼠们一起交流学习。

小土跟作者晓龙老师也认识许久,今天转载老师一篇关于gomonkey用法的文章。也容许我在这里给大家介绍一下晓龙老师。如对文章有看法也欢迎大家在留言区进行交流与讨论。

晓龙老师来自中兴通讯,敏捷技术教练,2020年和2021年公司十佳教练,Go语言知名打桩框架gomonkey作者,具有十多年软件架构和开发经验。近年来专注于PaaS和5G等大型平台软件的设计与开发,尤其对于TDD、 DDD 和微服务具有深刻的理解,对于大型软件的重构具有丰富的实战经验。曾经作为演讲嘉宾多次参加全球架构师峰会、全球C++及系统软件技术大会、领域驱动设计中国峰会和TiD质量竞争力大会等,广受好评。

目录

  • 引言

  • gomonkey 惯用法刷新

    • interface 惯用法刷新

    • method 惯用法刷新

    • func 惯用法刷新

    • func var 惯用法刷新

    • constructor 惯用法刷新

  • 小结

引言

gomonkey[1] 是笔者开源的一款 Go 语言 的打桩框架,目标是让用户在单元测试中低成本的完成打桩,从而将精力聚焦于业务功能的开发。gomonkey 接口友好,功能强大,目前已被很多项目使用,用户遍及世界多个国家。

近一年,在诸多用户的共同努力下,gomonkey 社区发展的很快,连续发布了 8 个版本,不仅优化了一些基础特性,而且还新增了很多扩展特性,非常实用接地气。与此同时,gomonkey 的 star 数从 0.5k 跃升到了 1.1k,受到了国内外 gopher 的广泛赞赏和肯定。

6a1fc60504dbc90f6efa7f5dca2ee332.png
gomonkey.png

gomonkey 新增或优化的主要特性汇总:

特性分类贡献者备注
全面支持 arm64 架构新增hengwu0PR55 PR58
全面支持为 private method 打桩了新增hengwu0 lockdown56PR65 PR67 PR85
全面支持 386 架构新增segdumpingPR75
支持为 method 打桩时不传入receiver优化AVOliliPR78
支持为 func/func var/method 打桩时直接指定返回值新增AVOliliPR78
支持为 method 打桩时不必转化为reflect.Type类型,同时兼容原有的用法优化AVOliliPR83
支持为 method 打桩不传入receiver时函数可为变参优化punchioPR90

感谢所有 gomonkey 的贡献者,每一个特性都凝结着大家的心血和汗水。虽然我们不曾见过,但彼此心往一处想,劲往一处使,共同推动 gomonkey 社区持续发展,不断繁荣,从一个胜利走向另一个胜利。

在众多新特性中,gomonkey 全面支持 arm64 架构 是对业界影响最大的一个特性。去年笔者刚发布支持该特性的版本后,就很意外的收到了 Bouk 大神的来信:

3dac4f0e50bd76d36e568a5de0a0003d.png
letter.png

这里需要强调一下:Bouke 是 Go 语言 monkey[2]工程的创建者,在 2015 年就发表了 Go 语言猴子补丁原理[3]的文章。毫无疑问,gomonkey) 的思维底座主要来自 Bouke 的贡献,向他致敬,非常感谢!

如果你对 gomonkey 全面支持 arm64 架构感兴趣,可以进一步阅读笔者之前写的一篇文章《gomonkey 全面支持 arm64 了》[4]

gomonkey 惯用法刷新

gomonkey 基础特性列表如下:

  • 支持为一个函数打一个桩

  • 支持为一个成员方法打一个桩

  • 支持为一个全局变量打一个桩

  • 支持为一个函数变量打一个桩

  • 支持为一个函数打一个特定的桩序列

  • 支持为一个成员方法打一个特定的桩序列

  • 支持为一个函数变量打一个特定的桩序列

想要了解 gomonkey 的这些基础特性,可以参考几年前笔者的一篇文章《gomonkey 1.0 正式发布》[5]

interface 惯用法刷新

之前很多 gopher 习惯使用 GoMock 框架对 interface 进行打桩,笔者当时也写了一篇文章《GoMock框架使用指南》[6]。后来有一些 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打桩了》[7]

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 惯用法的十大刷新,希望读者可以快速掌握,并能及时将学到的技能应用到开发者测试的具体实践中去,使得测试用例的开发效率和表达力都进一步得到提升。

参考资料

[1]

gomonkey: https://github.com/agiledragon/gomonkey

[2]

monkey: https://github.com/bouk/monkey

[3]

猴子补丁原理: https://bou.ke/blog/monkey-patching-in-go/

[4]

《gomonkey 全面支持 arm64 了》: https://www.jianshu.com/p/59d5ccf3fcb1

[5]

《gomonkey 1.0 正式发布》: https://www.jianshu.com/p/633b55d73ddd

[6]

《GoMock框架使用指南》: https://www.jianshu.com/p/f4e773a1b11f

[7]

《gomonkey支持为private method打桩了》: https://www.jianshu.com/p/7546e788613b

资料下载

点击下方卡片关注公众号,发送特定关键字获取对应精品资料!

  • 回复「电子书」,获取入门、进阶 Go 语言必看书籍。

  • 回复「视频」,获取价值 5000 大洋的视频资料,内含实战项目(不外传)!

  • 回复「路线」,获取最新版 Go 知识图谱及学习、成长路线图。

  • 回复「面试题」,获取四哥精编的 Go 语言面试题,含解析。

  • 回复「后台」,获取后台开发必看 10 本书籍。

对了,看完文章,记得点击下方的卡片。关注我哦~ 👇👇👇

如果您的朋友也在学习 Go 语言,相信这篇文章对 TA 有帮助,欢迎转发分享给 TA,非常感谢!a4792c0ac791f7a67af2d7d74aeb7902.png

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Go语言中,你可以使用`gomonkey`库来进行接口的模拟(mock)。`gomonkey`是一个用于在测试中进行mock的库,它可以帮助你模拟接口的实现,并在测试过程中替换原始的接口。 下面是一个简单的示例,演示了如何使用`gomonkey`库来模拟接口: ```go package main import ( "fmt" "github.com/agiledragon/gomonkey" ) // 定义一个接口 type MyInterface interface { DoSomething() string } // 实现接口的结构体 type MyStruct struct{} func (m *MyStruct) DoSomething() string { return "Real implementation" } func main() { // 创建一个gomonkey实例 patch := gomonkey.NewPatches() // 创建一个模拟对象 mock := &MyStruct{} // 注册模拟对象到gomonkey实例中 patch.ApplyMethod(reflect.TypeOf(mock), "DoSomething", func(_ *MyStruct) string { return "Mocked implementation" }) // 在函数结束时恢复被修改的函数 defer patch.Reset() // 调用被模拟的接口方 fmt.Println(mock.DoSomething()) // 输出: Mocked implementation } ``` 在上述代码中,我们首先定义了一个接口`MyInterface`和一个实现了该接口的结构体`MyStruct`。然后,我们使用`gomonkey`库创建了一个`Patches`对象,并通过`ApplyMethod`方将模拟对象注册到该`Patches`对象中。 在`ApplyMethod`函数中,我们指定了要替换的方,以及一个匿名函数作为替代实现。在这个示例中,我们将`DoSomething`方的实现替换为返回固定字符串"Mocked implementation"。最后,我们通过调用模拟对象的方来验证替代实现是否生效。 在函数结束时,我们使用`Reset`方来恢复被修改的函数,以确保不会影响其他部分的测试。 需要注意的是,`gomonkey`库可以用于在测试中模拟函数、方和全局变量,不仅限于接口的模拟。具体使用方式可以根据实际需求进行调整。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值