全能测试库Go Monkey,已支持arm64,还有了这些功能增强
引言
gomonkey[1] 是笔者开源的一款 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[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,还有了这些功能增强相关推荐
- 功能安全 李艳文_中国汽车功能安全测试库首次成果发布会成功召开
2019年9月25-26日,汽车工程研究院(以下简称"工程院")牵头搭建的中国汽车功能安全测试库(以下简称"测试库")首次成果发布会在江西上饶"汽标委 ...
- Robot Framework(十四) 扩展RobotFramework框架——创建测试库
4.1创建测试库 Robot Framework的实际测试功能由测试库提供.有许多现有的库,其中一些甚至与核心框架捆绑在一起,但仍然经常需要创建新的库.这个任务并不复杂,因为正如本章所示,Robot ...
- tdsql完全兼容mysql吗_金融级数据库 TDSQL:已支持日 3.6亿+ 的交易量,TPS 10万+
原标题:金融级数据库 TDSQL:已支持日 3.6亿+ 的交易量,TPS 10万+ 作者: 胡盼盼:微众银行数据库平台负责人.硕士毕业于华中科技大学,毕业后加入腾讯,任高级工程师,从事分布式存储与云数 ...
- Gremlins.js – 模拟用户随机操作的 JS 测试库
Gremlins.js 是基于 JavaScript 编写的 Monkey 测试库,支持 Node.js 平台和浏览器中使用.Gremlins.js 随机模拟用户操作:单击窗口中的任意位置,在表格中输 ...
- Qt实用技巧:使用OpenCV库的视频播放器(支持播放器操作,如暂停、恢复、停止、时间、进度条拽托等...
点击上方"小白学视觉",选择加"星标"或"置顶" 重磅干货,第一时间送达 需求 使用OpenCV库的视频播放器(支持播放器操作,如暂停.恢复 ...
- 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 ...
- 阿里云开放国内首个云端数据库测试平台,云已成为数据库新标准;华为5G随行WiFi发布;科大讯飞推出 AI 专用语音芯片系列……...
戳蓝字"CSDN云计算"关注我们哦! 嗨,大家好,重磅君带来的[云重磅]特别栏目,如期而至,每周五第一时间为大家带来重磅新闻.把握技术风向标,了解行业应用与实践,就交给我重磅君吧! ...
- oracle 测试库搭建,Oracle Study之--通过RMAN克隆测试库
Oracle Study之--通过RMAN克隆测试库 通过使用数据库备份,DBA可以在同一服务器或其它服务器上建立副本数据库.这个副本数据库可以和主数据库有相同的名称(拷贝)或与主数据库名称不同(克隆 ...
- ios 编译openssl支持arm64(转)
最近在编译支付宝 快捷支付(无线) ios 端的时候发现demo不支持arm64.在网上找了下,看到客服说是openssl的库文件不支持arm64,于是自己编译了支持arm64的库文件,发现还是不行, ...
最新文章
- AI一分钟 | 华为余承东携Mate 10高调亮相CES,不惧美国运营商放鸽子;日本推“手掌支付”服务,竟靠手相和手掌静脉识别
- jQuery源码dom ready分析
- mysql 5.6到percona 5.6小版本升级
- 1、请简述DNS的作用,并说明当你输入网址“www.nxtc.edu.cn“按下回车后,DNS是怎么工作的?(关键步骤可以给出相应图示) 2、详细描述域名劫持攻击的过程及防御方式。
- micropython web ws2812_MicroPython实例之TPYBoard v102炫彩跑马灯WS2812B
- ++递归 字符串全排列_超全递归技巧整理,这次一起拿下递归
- jQuery html表格排序插件:tablesorter
- 单链表算法设计(含大厂面试题)
- Python极其简易音乐播放器
- 【COCOS2D-HTML5 开发之一】新建HTML5项目及简单阐述与COCOS2D/X引擎关系
- 【MDVRP】基于matlab水滴算法求解多仓库车辆路径规划问题【含Matlab源码 1310期】
- export学习笔记(Es6阮一峰)
- mmsi是代表船舶什么_船舶常见的一些缩写
- ESD(静电释放)下半部分
- <机器学习 房价预测 >对贝壳租房网 信息爬取 及处理。
- 做一款仿打车软件需要多少钱?
- 唯美烟花特效登录页面,我感觉自己又行了
- android放微信短视频文件,参考微信实现的短视频录像
- Mysql组复制(MGR)——前提及限制
- hadoop安装-redhat