文章目录
- 基础回顾
- Mock
- Mock是什么
- 安装gomock
- Mock使用
- 1. 创建user.go源文件
- 2. 使用mockgen生成对应的Mock文件
- 3. 使用mockgen命令生成后在对应包mock下可以查看生成的mock文件
- 4. 编写测试代码
- 5. 运行代码并查看输出
- Gomonkey
- Gomonkey优势
- 安装
- 使用
- 对函数进行monkey
- 对结构体中方法进行monkey
- 对全局变量进行monkey
基础回顾
测试:
● 建立完善的回归测试,如果采用微服务,可以限制测试范围,只需要保证独立的微服务内部功能正常,因为微服务对外暴露的是接口。
● 为什么要将测试文件和源文件写到一个包中:便于测试包级别的示例,方法。
- go test 命令是一个按照一定约定和组织的测试代码驱动测试,在包目录下 所有以_test.go结尾的文件都会被视为测试文件。并且 _test.go结尾的文件不会被go build 编译到可执行文件中。
Mock
Mock是什么
Mock是单元测试中常见的一种技术,就是在测试过程中,对于一些不容易构造或者获取的对象,创建一个Mock对象来模拟对象的行为,从而把测试与测试边界以外的对象隔离开。
优点:
团队并行工作
测试驱动开发 TDD (Test-Driven Development)
测试覆盖率
隔离系统
缺点
- Mock不是万能的,使用Mock也存在着风险,需要根据项目实际情况和具体需要来确定是否选用Mock。
- 测试过程中如果大量使用Mock,mock测试的场景失去了真实性,可能会导致在后续的系统性测试时才发现bug,使得缺陷发现的较晚,可能会造成后续修复成本更大
Mock广泛应用于接口类的方法的生成。 针对接口可以使用mock,针对不是接口的函数或者变量使用下面的monkey。
安装gomock
go get github.com/golang/mock/gomock
go get github.com/golang/mock/mockgen
go install github.com/golang/mock/mockgen@v1.6.0
安装完成后执行mockgen命令查看是否生效
Mock使用
1. 创建user.go源文件
// Package mock ------------------------------------------------------------
// @file : user.go
// @author : WeiTao
// @contact : 15537588047@163.com
// @time : 2024/10/4 13:16
// ------------------------------------------------------------
package mock
import "context"
type User struct {
Mobile string
Password string
NickName string
}
type UserServer struct {
Db UserData
}
func (us *UserServer) GetUserByMobile(ctx context.Context, mobile string) (User, error) {
user, err := us.Db.GetUserByMobile(ctx, mobile)
if err != nil {
return User{}, err
}
if user.NickName == "TestUser" {
user.NickName = "TestUserModified"
}
return user, nil
}
type UserData interface {
GetUserByMobile(ctx context.Context, mobile string) (User, error)
}
上述代码中的UserData是一个Interface{}类型,后面使用Mock生成的对象就是此接口对象,生成后可以将UserData接口中的方法GetUserByMoblie方法实现黑盒,从而无需关注具体实现细节,直接可以设置此函数对应的返回值。
2. 使用mockgen生成对应的Mock文件
mockgen使用:
// 源码模式
mockgen -source 需要mock的文件名 -destination 生成的mock文件名 -package 生成mock文件的包名
// 参考示例:
mockgen -source user.go -destination=./mock/user_mock.go -package=mock
3. 使用mockgen命令生成后在对应包mock下可以查看生成的mock文件
// Code generated by MockGen. DO NOT EDIT.
// Source: user.go
// Package mock is a generated GoMock package.
package mock
import (
context "context"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockUserData is a mock of UserData interface.
type MockUserData struct {
ctrl *gomock.Controller
recorder *MockUserDataMockRecorder
}
// MockUserDataMockRecorder is the mock recorder for MockUserData.
type MockUserDataMockRecorder struct {
mock *MockUserData
}
// NewMockUserData creates a new mock instance.
func NewMockUserData(ctrl *gomock.Controller) *MockUserData {
mock := &MockUserData{ctrl: ctrl}
mock.recorder = &MockUserDataMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockUserData) EXPECT() *MockUserDataMockRecorder {
return m.recorder
}
// GetUserByMobile mocks base method.
func (m *MockUserData) GetUserByMobile(ctx context.Context, mobile string) (User, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetUserByMobile", ctx, mobile)
ret0, _ := ret[0].(User)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetUserByMobile indicates an expected call of GetUserByMobile.
func (mr *MockUserDataMockRecorder) GetUserByMobile(ctx, mobile interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserByMobile", reflect.TypeOf((*MockUserData)(nil).GetUserByMobile), ctx, mobile)
}
观察上述Mock文件可以看到实现了GetUserByMobile方法等。
4. 编写测试代码
// ------------------------------------------------------------
// package test
// @file : user_test.go
// @author : WeiTao
// @contact : 15537588047@163.com
// @time : 2024/10/4 13:23
// ------------------------------------------------------------
package mock
import (
"context"
"github.com/golang/mock/gomock"
"testing"
)
func TestGetUserByMobile(t *testing.T) {
// mock准备工作
ctl := gomock.NewController(t)
defer ctl.Finish()
userData := NewMockUserData(ctl)
userData.EXPECT().GetUserByMobile(gomock.Any(), "15023076751").Return(
User{
Mobile: "15023076751",
Password: "123456",
NickName: "TestUser",
},
nil,
)
// 实际调用过程
userServer := UserServer{
Db: userData,
}
user, err := userServer.GetUserByMobile(context.Background(), "15023076751")
// 判断过程
if err != nil {
t.Error("GetUserByMobile error:", err)
}
if user.Mobile != "15023076751" || user.Password != "123456" || user.NickName != "TestUserModified" {
t.Error("GetUserByMobile result is not expected.")
}
}
5. 运行代码并查看输出
GOROOT=/usr/local/go #gosetup
GOPATH=/home/wt/Backend/go/goprojects #gosetup
/usr/local/go/bin/go test -c -o /home/wt/.cache/JetBrains/GoLand2024.2/tmp/GoLand/___TestGetUserByMobile_in_golearndetail_test_mock.test golearndetail/test/mock #gosetup
/usr/local/go/bin/go tool test2json -t /home/wt/.cache/JetBrains/GoLand2024.2/tmp/GoLand/___TestGetUserByMobile_in_golearndetail_test_mock.test -test.v=test2json -test.paniconexit0 -test.run ^\QTestGetUserByMobile\E$
=== RUN TestGetUserByMobile
--- PASS: TestGetUserByMobile (0.00s)
PASS
Process finished with the exit code 0
Gomonkey
Gomonkey优势
上面使用mockgen生成对应的mock文件缺点非常明显,只能对于接口类的函数进行mock,然而实际项目并非所有函数都是接口类函数,大部分是内部使用的临时函数以及变量等,此时对这些函数以及变量无法使用mockgen生成对应的mock文件,此时可以使用另一个工具gomonkey
链接:https://github.com/agiledragon/gomonkey
安装
参考官网安装说明:
$ go get github.com/agiledragon/gomonkey/v2@v2.11.0
使用
对函数进行monkey
- 编写函数
package monkey
func networkCompute(a, b int) (int, error) {
c := a + b
return c, nil
}
func compute(a, b int) (int, error) {
c, err := networkCompute(a, b)
return c, err
}
- 编写测试用例
// 对函数进行mock
func Test_compute(t *testing.T) {
patches := gomonkey.ApplyFunc(networkCompute, func(a, b int) (int, error) {
return 8, nil
})
defer patches.Reset()
sum, err := compute(1, 2)
if err != nil {
t.Error("Error occurred:", err)
}
fmt.Printf("sum : %d\n", sum)
if sum != 8 {
t.Error("Error occurred in sum:", err)
}
}
在使用gomonkey运行测试用例的时候,直接run会报内联错误,解决方法有两个:
- 在终端执行命令go test时加上参数:go test -gcflags=all=-l
- 在Goland编辑器添加对应go运行变量:-gcflags=all=-l
加上 "all=-N -l"和”=all=-l"效果相同。
- 运行结果
/usr/local/go/bin/go tool test2json -t /home/wt/.cache/JetBrains/GoLand2024.2/tmp/GoLand/___7Test_compute_in_golearndetail_test_monkey.test -test.v=test2json -test.paniconexit0 -test.run ^\QTest_compute\E$
=== RUN Test_compute
sum : 8
--- PASS: Test_compute (0.00s)
PASS
Process finished with the exit code 0
对结构体中方法进行monkey
- 编写代码
type Compute struct{}
func (c *Compute) NetworkCompute(a, b int) (int, error) {
sum := a + b
return sum, nil
}
func (c *Compute) Compute(a, b int) (int, error) {
sum, err := c.NetworkCompute(a, b)
return sum, err
}
- 编写测试用例
// 对结构体中的方法进行mock
func Test_Compute_NetworkCompute(t *testing.T) {
var compute *Compute
patches := gomonkey.ApplyMethod(reflect.TypeOf(compute), "NetworkCompute", func(_ *Compute, a, b int) (int, error) {
return 10, nil
})
defer patches.Reset()
compute = &Compute{}
sum, err := compute.Compute(3, 2)
if err != nil {
t.Error("Error occurred:", err)
}
fmt.Printf("sum : %d\n", sum)
if sum != 10 {
t.Error("Error occurred in sum:", err)
}
}
- 运行结果
/usr/local/go/bin/go tool test2json -t /home/wt/.cache/JetBrains/GoLand2024.2/tmp/GoLand/___2Test_Compute_NetworkCompute_in_golearndetail_test_monkey.test -test.v=test2json -test.paniconexit0 -test.run ^\QTest_Compute_NetworkCompute\E$
=== RUN Test_Compute_NetworkCompute
sum : 10
--- PASS: Test_Compute_NetworkCompute (0.00s)
PASS
Process finished with the exit code 0
对全局变量进行monkey
代码展示:
var num = 5
// 对变量进行mock
func Test_GlobalVal(t *testing.T) {
patches := gomonkey.ApplyGlobalVar(&num, 10)
defer patches.Reset()
if num != 10 {
t.Error("Error occurred in num:mock failure", num)
}
}