问题的由来
在 gomonkey 社区,用户 JiangHanChao 提了一个 issue,如下图所示:
issue-82.png
恰巧,笔者身边的同事和朋友也有咨询过该问题,因此就想着写篇短文来统一回复。
解决方案
gomonkey 在实现打桩功能时,仅在函数头植入了几行汇编,再增加一个计数功能的话,对某些简短的函数打桩后会破坏下一个函数的序言,进而造成运行时段错误,并且这个错误没法在打桩时就发现并告知用户。
基于上述考虑,gomonkey 对桩计数不能在框架层面统一解决,可以考虑在用户侧来解决,即在测试代码中定义桩计数器结构体,每个测试开始时初始化该结构体,每个测试结束前断言该结构体中的字段。
典型案例
将 issue-82 中给出的示例作为典型案例,供大家参考!
(1)产品代码保持不变
package moduleA
func FuncA() error {
return nil
}
func FuncB() error {
return nil
}
func FuncC() error {
if err := FuncA(); err != nil {
return err
}
if err := FuncB(); err != nil {
return err
}
return nil
}
(2)在测试代码中应用本文解决方案
package moduleA
import (
"errors"
"testing"
. "github.com/agiledragon/gomonkey/v2"
. "github.com/smartystreets/goconvey/convey"
)
// 在测试代码中定义桩计数器结构体
type monkeyMetric struct {
callFuncATimes int
callFuncBTimes int
}
func TestFuncC(t *testing.T) {
// 每个测试开始时初始化该结构体
var metric monkeyMetric
err := errors.New("test")
Convey("test", t, func() {
patches := ApplyFunc(FuncA, func() error {
metric.callFuncATimes++
return err
})
patches.ApplyFunc(FuncB, func() error {
metric.callFuncBTimes++
return err
})
defer patches.Reset()
e := FuncC()
So(e, ShouldEqual, err)
// 每个测试结束前断言该结构体中的字段
So(metric.callFuncATimes, ShouldEqual, 1)
So(metric.callFuncBTimes, ShouldEqual, 1)
})
}
(3)执行该测试用例,结果符合预期
测试用例执行结果:
=== RUN TestFuncC
test ✔✔✘
Failures:
* /Users/zhangxiaolong/Desktop/D/go-workspace/gomonkey/test/module_a_test.go
Line 33:
Expected: '1'
Actual: '0'
(Should be equal)
用户在 issue-82 中的期望:
user-expect.png
小结
用户期望可以在框架层面解决一切共性问题,这本属于合理的诉求,但现实有时很骨感,在种种约束下必须改变思路,而在用户侧较优雅的解决相关问题也是一种不错的选择。
本文针对 gomonkey 用户,给出了如何对桩进行计数的解决方案,并提供了典型案例,希望对读者有一定的帮助!