使用示例示例(1)使用方法一全局函数调用,其余使用结构体方法调用。

gomonkey.ApplyFunc()

相关代码在gitee代码仓库的示例代码中,仓库地址请看博客开头

参数示例:

// 第一个参数:函数名
// 第二个参数:打桩函数,入参和出参要保持和被打桩函数保持一致
func ApplyFunc(target, double interface{}) *Patches {
return create().ApplyFunc(target, double)
}

user.go

package func_demo
import "tracer/model/sqlmock_demo"
// GetUserInfo 查询所有用户信息
func GetUserInfo() (interface{}, error) {
obj, err := sqlmock_demo.GetAllUser()
if err != nil {
return nil, err
}
return obj, nil
}

user_test.go

package func_demo
import (
"errors"
"fmt"
"github.com/agiledragon/gomonkey/v2"
"gorm.io/gorm"
"testing"
"tracer/model"
"tracer/model/sqlmock_demo"
)
// TestUserInfo 单个函数通过循环覆盖所有测试场景
func TestUserInfo(t *testing.T) {
// 1. 定义测试用例结构体:封装输入(打桩参数)和预期输出
type testCase struct {
name string // 用例名称,便于排查错误
mockUsers []sqlmock_demo.UserInfo // 打桩 GetAllUser 返回的用户列表
mockErr error // 打桩 GetAllUser 返回的错误
expectedErr error // 预期 UserInfoDao 返回的错误
expectedNil bool // 预期 UserInfoDao 返回的数据是否为 nil
expectedCount int // 预期返回的用户数量(正常场景有效)
}
// 2. 构造所有测试用例(正常场景 + 异常场景)
testCases := []testCase{
{
name: "正常场景-返回2个用户",
mockUsers: []sqlmock_demo.UserInfo{
{
Model: gorm.Model{ID: 1},
UserName: "zhangsan",
Password: "123456",
Phone: "13800138000",
Email: "zhangsan@test.com",
},
{
Model: gorm.Model{ID: 2},
UserName: "lisi",
Password: "654321",
Phone: "13900139000",
Email: "lisi@test.com",
},
},
mockErr: nil,
expectedErr: nil,
expectedNil: false,
expectedCount: 2,
},
{
name: "正常场景-返回空用户列表",
mockUsers: []sqlmock_demo.UserInfo{},
mockErr: nil,
expectedErr: nil,
expectedNil: false,
expectedCount: 0,
},
{
name: "异常场景-GORM记录不存在错误",
mockUsers: nil,
mockErr: gorm.ErrRecordNotFound,
expectedErr: gorm.ErrRecordNotFound,
expectedNil: true,
expectedCount: 0,
},
{
name: "异常场景-自定义查询错误",
mockUsers: nil,
mockErr: errors.New("数据库连接超时"),
expectedErr: errors.New("数据库连接超时"),
expectedNil: true,
expectedCount: 0,
},
}
// 3. 循环执行所有测试用例
for _, tc := range testCases {
model.InitDb()
// t.Run:为每个用例创建独立的测试上下文,互不干扰,便于定位用例错误
t.Run(tc.name, func(t *testing.T) {
// 步骤1:对 GetAllUser 进行动态打桩(每个用例独立打桩,避免相互影响)
// 使用ApplyFunc打桩跨包函数
patches := gomonkey.ApplyFunc(sqlmock_demo.GetAllUser, func() ([]sqlmock_demo.UserInfo, error) {
// 返回当前用例预设的模拟数据和错误
return tc.mockUsers, tc.mockErr
})
defer patches.Reset() // 每个用例执行完毕后重置打桩,避免污染其他用例
// 步骤2:执行待测试函数 GetUserInfo
_, err := GetUserInfo()
if err != nil {
fmt.Println(err)
}
})
}
}

命令行执行命令

go test -cover -gcflags=all=-l -covermode=atomic

结果:

PS D:\wyl\workspace\go\tracer\logic\func_demo> go test -cover
PASS
coverage: 75.0% of statements
ok tracer/logic/func_demo 0.088s

如果报错,这个问题是数据库中不存在表:

Error 1146 (42S02): Table 'tracer.user_info' doesn't exist

(2)结构体方法打桩方法

gomonkey.ApplyMethod()、gomonkey.ApplyMethodFunc()

区别:

  • 匹配方式不同ApplyMethod是名称匹配,ApplyMethodFunc是函数本体匹配
  • 传参核心不同ApplyMethod必须传reflect.Type+方法名字符串ApplyMethodFunc直接传原方法函数不需要反射
  • 底层逻辑不同ApplyMethod是「反射查找方法」,ApplyMethodFunc是「直接绑定方法函数」,后者性能更高

gomonkey.ApplyMethod() 参数示例:

// 第一个参数:要打桩的方法所属的类型,通过 reflect.TypeOf(实例) 获取,区分值接收者 / 指针接收者
// 第二个参数:要打桩的方法名,字符串格式、大小写敏感,必须和原方法名完全一致
// 第三个参数:打桩方法,入参和出参要保持和被打桩方法保持一致,但需注意需要额外传入结构体类型且必须是第一个参数
func ApplyMethod(target interface{}, methodName string, double interface{}) *Patches {
return create().ApplyMethod(target, methodName, double)
}

gomonkey.ApplyMethodFunc() 参数示例:

// 第一个参数:要打桩的方法所属的类型,通过 reflect.TypeOf(实例) 获取,区分值接收者 / 指针接收者
// 第二个参数:要打桩的方法名,字符串格式、大小写敏感,必须和原方法名完全一致
// 第三个参数:打桩方法,入参和出参要保持和被打桩方法保持一致,不需要额外参数
func ApplyMethodFunc(target interface{}, methodName string, doubleFunc interface{}) *Patches {
return create().ApplyMethodFunc(target, methodName, doubleFunc)
}

注意:可以不用写reflect.TypeOf(实例),如果了解方法所属类型,可以直接写方法类型,而不用reflect.TypeOf()在获取一次

gomonkey.ApplyMethod()

method_demo.go

type MethodDemo struct {
}
func (m MethodDemo) MethodDemo(ret string) {
fmt.Println("MethodDemo:", ret)
}

method_demo_test.go

// ========== 基于 gomonkey.ApplyMethod() 的单元测试 ==========
func TestMethodDemo(t *testing.T) {
// 1. 初始化结构体实例(当前结构体无成员变量,直接实例化即可)
md := MethodDemo{}
patches := gomonkey.NewPatches()
// 铁律:延迟撤销打桩,防止污染其他测试用例,必写!
defer patches.Reset()
// 2. 核心:使用 gomonkey.ApplyMethod() 对【值接收者方法】打桩
// 第一个参数:reflect.TypeOf(实例) → 因为是值接收者,直接传值类型实例即可
// 第二个参数:被打桩的方法名字符串(严格和原方法名一致,大小写敏感)
// 第三个参数:mock桩函数 → 入参/返回值 必须和原方法完全一致
patches.ApplyMethod(
// 可以直接写 MethodDemo{}
reflect.TypeOf(md),
"MethodDemo",
func(m MethodDemo, ret string) {
// 自定义的mock逻辑,替代原方法的 fmt.Println 逻辑
t.Log("mock执行成功,入参ret:", ret)
},
)
// 3. 调用原方法,验证打桩是否生效
md.MethodDemo("hello gomonkey")
}

命令行执行命令

go test -run "^TestMethodDemo$" -cover

结果:

PS D:\wyl\workspace\go\tracer\logic\method_demo> go test -run "^TestMethodDemo$" -cover
MethodDemo: hello gomonkey
PASS
coverage: 50.0% of statements
ok tracer/logic/method_demo 0.303s
gomonkey.ApplyMethodFunc()

method_func_demo.go

type MethodFuncDemo struct {
}
func (m MethodFuncDemo) MethodFuncDemo(ret string) {
fmt.Println("MethodFuncDemo:", ret)
}

method_func_demo_test.go

// ========== 【结构体值类型绑定】对应的单元测试(纯值类型,无任何指针语法) ==========
func TestMethodFuncDemo(t *testing.T) {
// 1. 初始化【结构体值类型实例】 核心✅ 无指针&,纯结构体类型绑定
mfd := MethodFuncDemo{}
patches := gomonkey.NewPatches()
// 铁律:延迟撤销打桩,防止污染其他测试用例,必写!
defer patches.Reset()
// 2. 核心:gomonkey.ApplyMethodFunc 三参数打桩【结构体值类型绑定】
// 三参数固定规则:值类型 = 值实例 + 方法名字符串 + 值类型桩函数
patches.ApplyMethodFunc(
mfd, // 参数1:结构体值类型实例(核心,纯值绑定)
"MethodFuncDemo", // 参数2:方法名字符串(和值接收者方法名一致)
func(ret string) { // 参数3:桩函数【无*号,纯结构体值类型入参】✅必匹配
// 桩函数第一个入参必须是:纯结构体类型 MethodFuncDemo,无任何指针
t.Log("✅ 结构体值类型绑定打桩生效,入参ret = ", ret)
},
)
// 3. 调用【值接收者方法】,验证结构体值类型绑定打桩结果
mfd.MethodFuncDemo("hello gomonkey 结构体值类型绑定")
}

命令行执行命令

go test -run "^TestMethodFuncDemo$" -gcflags=all=-l

结果:

PS D:\wyl\workspace\go\tracer\logic\method_demo> go test -run "^TestMethodFuncDemo$"
MethodFuncDemo: hello gomonkey 结构体值类型绑定
PASS
ok tracer/logic/method_demo 0.243s
PS D:\wyl\workspace\go\tracer\logic\method_demo> go test -run "^TestMethodFuncDemo$" -gcflags=all=-l
PASS
ok tracer/logic/method_demo 0.219s