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/v2@v2.7.0
如何导入 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 的特性,暂不考虑开发计划。