引言

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

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

gomonkey.png

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 大神的来信:

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.SliceConvey("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.SliceConvey("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.SliceConvey("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.PrivateMethodStructpatches := 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] = 4return nil})output, err := fake.Exec("", "")So(err, ShouldEqual, nil)So(output, ShouldEqual, outputExpect)var m map[int]interr = 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] = 4return 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]interr = 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 []OutputCellfor _, 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招聘公众号,获取Go专题大厂内推面经简历股文等相关资料可回复和点击导航查阅。

关于 Go 单元测试的通关指南,除了在公号历史文章里能找到,我还放到了整理的Go开发参考书中,这样总览全局更直观些。

电子书的获取方式:公众号「网管叨bi叨」回复 gocookbook 即可获得。

- END -

扫码关注公众号「网管叨bi叨」

给网管个星标,第一时间吸我的知识 

全能测试库Go Monkey,已支持arm64,还有了这些功能增强相关推荐

  1. 功能安全 李艳文_中国汽车功能安全测试库首次成果发布会成功召开

    2019年9月25-26日,汽车工程研究院(以下简称"工程院")牵头搭建的中国汽车功能安全测试库(以下简称"测试库")首次成果发布会在江西上饶"汽标委 ...

  2. Robot Framework(十四) 扩展RobotFramework框架——创建测试库

    4.1创建测试库 Robot Framework的实际测试功能由测试库提供.有许多现有的库,其中一些甚至与核心框架捆绑在一起,但仍然经常需要创建新的库.这个任务并不复杂,因为正如本章所示,Robot ...

  3. tdsql完全兼容mysql吗_金融级数据库 TDSQL:已支持日 3.6亿+ 的交易量,TPS 10万+

    原标题:金融级数据库 TDSQL:已支持日 3.6亿+ 的交易量,TPS 10万+ 作者: 胡盼盼:微众银行数据库平台负责人.硕士毕业于华中科技大学,毕业后加入腾讯,任高级工程师,从事分布式存储与云数 ...

  4. Gremlins.js – 模拟用户随机操作的 JS 测试库

    Gremlins.js 是基于 JavaScript 编写的 Monkey 测试库,支持 Node.js 平台和浏览器中使用.Gremlins.js 随机模拟用户操作:单击窗口中的任意位置,在表格中输 ...

  5. Qt实用技巧:使用OpenCV库的视频播放器(支持播放器操作,如暂停、恢复、停止、时间、进度条拽托等...

    点击上方"小白学视觉",选择加"星标"或"置顶" 重磅干货,第一时间送达 需求 使用OpenCV库的视频播放器(支持播放器操作,如暂停.恢复 ...

  6. react jest测试_如何使用React测试库和Jest开始测试React应用

    react jest测试 Testing is often seen as a tedious process. It's extra code you have to write, and in s ...

  7. 阿里云开放国内首个云端数据库测试平台,云已成为数据库新标准;华为5G随行WiFi发布;科大讯飞推出 AI 专用语音芯片系列……...

    戳蓝字"CSDN云计算"关注我们哦! 嗨,大家好,重磅君带来的[云重磅]特别栏目,如期而至,每周五第一时间为大家带来重磅新闻.把握技术风向标,了解行业应用与实践,就交给我重磅君吧! ...

  8. oracle 测试库搭建,Oracle Study之--通过RMAN克隆测试库

    Oracle Study之--通过RMAN克隆测试库 通过使用数据库备份,DBA可以在同一服务器或其它服务器上建立副本数据库.这个副本数据库可以和主数据库有相同的名称(拷贝)或与主数据库名称不同(克隆 ...

  9. ios 编译openssl支持arm64(转)

    最近在编译支付宝 快捷支付(无线) ios 端的时候发现demo不支持arm64.在网上找了下,看到客服说是openssl的库文件不支持arm64,于是自己编译了支持arm64的库文件,发现还是不行, ...

最新文章

  1. AI一分钟 | 华为余承东携Mate 10高调亮相CES,不惧美国运营商放鸽子;日本推“手掌支付”服务,竟靠手相和手掌静脉识别
  2. jQuery源码dom ready分析
  3. mysql 5.6到percona 5.6小版本升级
  4. 1、请简述DNS的作用,并说明当你输入网址“www.nxtc.edu.cn“按下回车后,DNS是怎么工作的?(关键步骤可以给出相应图示) 2、详细描述域名劫持攻击的过程及防御方式。
  5. micropython web ws2812_MicroPython实例之TPYBoard v102炫彩跑马灯WS2812B
  6. ++递归 字符串全排列_超全递归技巧整理,这次一起拿下递归
  7. jQuery html表格排序插件:tablesorter
  8. 单链表算法设计(含大厂面试题)
  9. Python极其简易音乐播放器
  10. 【COCOS2D-HTML5 开发之一】新建HTML5项目及简单阐述与COCOS2D/X引擎关系
  11. 【MDVRP】基于matlab水滴算法求解多仓库车辆路径规划问题【含Matlab源码 1310期】
  12. export学习笔记(Es6阮一峰)
  13. mmsi是代表船舶什么_船舶常见的一些缩写
  14. ESD(静电释放)下半部分
  15. <机器学习 房价预测 >对贝壳租房网 信息爬取 及处理。
  16. 做一款仿打车软件需要多少钱?
  17. 唯美烟花特效登录页面,我感觉自己又行了
  18. android放微信短视频文件,参考微信实现的短视频录像
  19. Mysql组复制(MGR)——前提及限制
  20. hadoop安装-redhat

热门文章

  1. iframe高度自适应的方法
  2. blogger_如何向Blogger投球
  3. 怎样进入 Windows 10 的“开发者模式”
  4. Mock.Js学习报告
  5. 2021-03-17-伪协议
  6. 计算机技术都幻灯片儿,13-计算机应用基础 幻灯片.ppt
  7. SWAN之ikev2协议config-payload-swapped配置测试
  8. python修改rgb值_Python替换或更改给定RGB或十六进制值的HSV值
  9. Struts2表单数据校验
  10. SQL中如何将月份转换为英文缩写