gomonkey支持为private method打桩了

引言

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

这几年陆续有多个 gopher 线上或线下咨询笔者 gomonkey 是否可以给私有方法(private method)打桩的问题,他们往往显得比较焦急,笔者也感同身受,能够体会到他们对该需求的渴望。但那时那刻,该需求实现难度很大,作者通常会答复暂时无法实现该需求,主要原因是 Go 反射的 API 不支持,具体就是 reflect 包有限制,即提供的 MethodByName 方法实现中调用了私有的 exportedMethods (可导出)方法,就是说私有方法(不可导出)在 MethodByName 这个 API 中查不到。

直到 gomonkey 全面支持 arm64 后,笔者感觉 gomonkey 已经开始引领 Go 语言的猴子补丁了。这时,恰好又有一些 gopher 站出来,希望 gomonkey 能够使用黑科技实现对私有方法打桩的需求。虽然这次与前几次是同样的需求,但此时此刻,作者突然有了一个想挑战一下的念头冒出来,于是就有了后续支持该需求的破局行动。

有的读者可能会有疑问:public method 是类暴露出来的 API,相对稳定,而 private method 类内部的具体实现细节,可能不稳定,同时给 public method 打桩已经足够,为什么还有给 private method 打桩的需求?或者说,给 private method 打桩到底会有什么价值?

笔者也同样思考过这个问题,结论是至少有一种场景,价值还是很大的:private method 封装了多 个下层操作,这些操作虽然都是 public method,但是对 private method 打桩只需打一次桩,而对多个下层操作的 public method 打桩需要打多次桩,并且这几个桩有关联。显然,这时对 private method 直接打桩会更高效,让开发者自测更美好

private method 接口设计

private method 需求实现的关键是要穿越 reflect 包的限制,需要在 gomonkey 内实现一个特定的反射包来与 Go 语言对接。但为 gomonkey 定制的反射包 creflect 与 Go 语言标准库的反射包 reflect 在内部数据结构上有一定的耦合,因此需要设计独立的 API 让用户使用,而不复用已有的 ApplyMethod,以便 creflect 包的变化仅仅影响 private method 需求。

按照 gomonkey 的惯例,private method 接口应该有两个:一个是函数接口,一个是方法接口

func ApplyPrivateMethod(target interface{}, methodName string, double interface{}) *Patches
func (this *Patches) ApplyPrivateMethod(target interface{}, methodName string, double interface{}) *Patches

说明:除过方法名字,参数和 ApplyMethod 一模一样,之所以用两个方法名,完全是为了隔离变化。

private method 接口使用方法

gomonkey 在测试目录增加了测试文件 apply_private_method_test.go 来引导读者写 private 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")
        })
    })

}

模拟的产品代码:

type PrivateMethodStruct struct {
}

func (this *PrivateMethodStruct) ok() bool {
    return this != nil
}

func (this *PrivateMethodStruct) Happy() string {
    if this.ok() {
        return "happy"
    }
    return "unhappy"
}

func (this PrivateMethodStruct) haveEaten() bool {
    return this != PrivateMethodStruct{}
}

func (this PrivateMethodStruct) AreYouHungry() string {
    if this.haveEaten() {
        return "I am full"
    }

    return "I am hungry"
}

运行测试:

zhangxiaolongdeMacBook-Pro:test zhangxiaolong$ go test -gcflags=all=-l apply_private_method_test.go -v
=== RUN   TestApplyPrivateMethod

  TestApplyPrivateMethod 
    patch private pointer method in the different package ✔
    patch private value method in the different package ✔


2 total assertions

--- PASS: TestApplyPrivateMethod (0.00s)
PASS
ok      command-line-arguments  

新版本说明

作者在 github 上发布了 gomonkey 的新版本 v2.7.0 来完整支持该特性:


gomonkey v2.7.0.png

如何获取 gomonkey 新版本?
假设你使用 go get 命令来获取 gomonkey v2.7.0:

$ go get github.com/agiledragon/gomonkey/[email protected]

如何导入 gomonkey 新版本?

import (
   "encoding/json"
   "testing"

   . "github.com/agiledragon/gomonkey/v2"
   . "github.com/smartystreets/goconvey/convey"
)

小结

gomonkey 穿越 reflect 包的限制,终于支持为 private method 打桩的特性了!该特性高效解决了诸多 gopher 这些年写测试在某些场景下打桩繁杂的困扰,使得 gomonkey 打桩更贴心,让开发者自测更美好!

gomonkey 专门为用户提供了新接口来使用 private method 打桩特性,从而将为 gomonkey 私人定制的反射包 creflect 的影响尽可能的局部化。

关于支持 private method sequence 的特性,暂不考虑开发计划。

你可能感兴趣的:(gomonkey支持为private method打桩了)