gomonkey不支持原生序列替换,需用闭包计数器或状态struct手动实现调用序控,每次ApplyFunc会覆盖前次patch,必须在测试内独占状态并调用Reset()防泄漏。

gomonkey 不能直接“序列替换”函数
gomonkey 本身不支持对同一个函数做多次、按调用顺序依次生效的不同桩(mock)——它没有内置的“桩队列”或“调用序号匹配”机制。所谓“序列替换”,实际是用户自己通过 ApplyFunc 或 ApplyMethod + 闭包状态变量模拟出来的行为。
常见错误现象是:写多个 ApplyFunc 覆盖同一函数,结果只有最后一个生效;或者试图用 defer 清理时顺序错乱,导致测试间污染。
- 每次调用
gomonkey.ApplyFunc都会覆盖前一次对该函数的 patch,不是叠加 - “序列”逻辑必须由你自己在桩函数体内用计数器、切片索引或状态机控制
- 务必在测试结束时调用
patch.Reset(),否则后续测试可能被残留 patch 影响
用闭包变量实现调用次数驱动的桩行为
最常用也最可控的方式:把期望返回值列表和当前调用计数封装进闭包,让桩函数每次调用时按序返回。适用于纯函数、无副作用、返回值可预设的场景。
func TestFooWithSequence(t *testing.T) {
values := []int{10, 20, 30}
var callCount int
patch := gomonkey.ApplyFunc(
foo, // 假设 foo 是 func() int
func() int {
if callCount >= len(values) {
panic("unexpected call # " + strconv.Itoa(callCount))
}
ret := values[callCount]
callCount++
return ret
},
)
defer patch.Reset()
<pre class="brush:php;toolbar:false;">// 触发三次调用
_ = foo()
_ = foo()
_ = foo()
// 第四次会 panic,便于发现超调用}
注意:callCount 必须定义在 patch 外部作用域(如测试函数内),不能放在闭包外全局或包级,否则多个测试并发时会冲突。
用 struct 实现带状态的复杂桩(含参数匹配与副作用)
当需要根据入参选择返回值、记录调用历史、或触发真实副作用(如写文件、改全局变量)时,闭包不够用,推荐封装成 struct。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
例如桩函数 http.Get,希望第一次返回 200 OK,第二次返回 timeout error:
type MockHTTPGet struct {
calls []struct{ url string; resp *http.Response; err error }
index int
}
<p>func (m <em>MockHTTPGet) Get(url string) (</em>http.Response, error) {
m.calls = append(m.calls, struct{ url string; resp *http.Response; err error }{
url: url,
resp: nil,
err: nil,
})</p><pre class="brush:php;toolbar:false;">switch m.index {
case 0:
m.index++
return &http.Response{StatusCode: 200}, nil
case 1:
m.index++
return nil, context.DeadlineExceeded
default:
panic("too many calls to http.Get")
}}
// 使用 mock := &MockHTTPGet{} patch := gomonkey.ApplyFunc(http.Get, mock.Get) defer patch.Reset()
这种写法清晰暴露了状态流转,也方便断言 mock.calls 内容是否符合预期。但要注意:struct 实例不能复用在多个测试中,每个测试应新建实例。
避免 patch 泄漏和并发冲突的硬性约束
gomonkey 的 patch 是全局生效的,且不自带同步保护。多 goroutine 并发跑测试时,若共用一个 patch 或共享状态变量,极易出错。
- 每个测试函数必须独占自己的
patch和状态变量(如callCount或mock实例) - 禁止在
init()或包级变量中提前 patch,所有 patch 必须在TestXxx函数内创建 - 不要依赖
Reset()的执行时机 —— 它只重置函数指针,不清理你闭包里的状态;状态清理要手动做(比如重置callCount = 0) - 如果测试用了
t.Parallel(),确保桩逻辑本身是线程安全的(例如用sync.Mutex包裹状态读写)
真正麻烦的从来不是怎么写桩,而是怎么让桩不跨测试泄漏、不被并发踩踏。这点比语法细节更值得花时间检查。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










