你该刷新gomonkey的惯用法了

引言

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容