Go语言使用Mock单元测试

简介

使用Go开发Web项目时利用Mock工具进行单元测试

安装工具

1
go install go.uber.org/mock/mockgen@latest

生成需要mock的实例

方式1:手敲命令行

1
2
3
4
mockgen -source=internal/service/user.go -package=svcmocks -destination=internal/service/mock/user.go
mockgen -source=internal/service/code.go -package=svcmocks -destination=internal/service/mock/code.go
# 如果需要创建第三方包的mock(Redis为例)
mockgen -package=redismocks -destination=internal/repository/cache/redismocks/cmdable.go github.com/redis/go-redis/v9 Cmdable

方式2:使用go:generate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 文件:generate.go
package main

//go:generate mockgen -source=internal/service/user.go -package=mocksvc -destination=mock/service/user.go
//go:generate mockgen -source=internal/service/code.go -package=mocksvc -destination=mock/service/code.go

//go:generate mockgen -source=internal/repository/user.go -package=mockrepo -destination=mock/repository/user.go
//go:generate mockgen -source=internal/repository/code.go -package=mockrepo -destination=mock/repository/code.go

//go:generate mockgen -source=internal/repository/cache/user.go -package=mockcache -destination=mock/repository/cache/user.go
//go:generate mockgen -source=internal/repository/cache/code.go -package=mockcache -destination=mock/repository/cache/code.go

//go:generate mockgen -source=internal/repository/dao/user.go -package=mockdao -destination=mock/repository/dao/user.go

// mock第三方依赖
//go:generate mockgen -package=mockredis -destination=mock/repository/cache/redis/cmdable.go github.com/redis/go-redis/v9 Cmdable

然后在目录下执行即可创建mock文件

1
go generate ./...

进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
func TestUserHandler_SignUp(t *testing.T) {
testCases := []struct {
// 该test的名字
name string
// NewUserHandler需要UserService和CodeService,这里返回mock的实例并进行EXPECT
mock func(ctrl *gomock.Controller) (service.UserService, service.CodeService)
// 请求参数
reqBody string
wantCode int
wantBody string
}{
{
name: "注册成功",
mock: func(ctrl *gomock.Controller) (service.UserService, service.CodeService) {
userSvc := svcmocks.NewMockUserService(ctrl)
userSvc.EXPECT().SignUp(gomock.Any(), domain.User{
Email: "[email protected]",
Password: "qwer#1234",
}).Return(nil)
codeSvc := svcmocks.NewMockCodeService(ctrl)
return userSvc, codeSvc
},
reqBody: `{
"email": "[email protected]",
"password": "qwer#1234"
}`,
wantCode: http.StatusOK,
wantBody: "注册成功",
},
{
name: "邮箱格式不正确",
mock: func(ctrl *gomock.Controller) (service.UserService, service.CodeService) {
userSvc := svcmocks.NewMockUserService(ctrl)
codeSvc := svcmocks.NewMockCodeService(ctrl)
return userSvc, codeSvc
},
reqBody: `{
"email": "hahaqq.com",
"password": "qwer#1234"
}`,
wantCode: http.StatusBadRequest,
wantBody: "邮箱格式不对",
},
{
name: "参数错误,Bind失败",
mock: func(ctrl *gomock.Controller) (service.UserService, service.CodeService) {
userSvc := svcmocks.NewMockUserService(ctrl)
codeSvc := svcmocks.NewMockCodeService(ctrl)
return userSvc, codeSvc
},
reqBody: `{
email": "hahaqq.com",
"password": "qwer#1234"
}`,
wantCode: http.StatusBadRequest,
wantBody: "参数错误",
},
{
name: "邮箱冲突",
mock: func(ctrl *gomock.Controller) (service.UserService, service.CodeService) {
userSvc := svcmocks.NewMockUserService(ctrl)
userSvc.EXPECT().SignUp(gomock.Any(), domain.User{
Email: "[email protected]",
Password: "qwer#1234",
}).Return(service.ErrDuplicateEmail)
codeSvc := svcmocks.NewMockCodeService(ctrl)
return userSvc, codeSvc
},
reqBody: `{
"email": "[email protected]",
"password": "qwer#1234"
}`,
wantCode: http.StatusInternalServerError,
wantBody: "邮箱冲突",
},
{
name: "系统错误",
mock: func(ctrl *gomock.Controller) (service.UserService, service.CodeService) {
userSvc := svcmocks.NewMockUserService(ctrl)
userSvc.EXPECT().SignUp(gomock.Any(), domain.User{
Email: "[email protected]",
Password: "qwer#1234",
}).Return(errors.New("随便一个异常"))
codeSvc := svcmocks.NewMockCodeService(ctrl)
return userSvc, codeSvc
},
reqBody: `{
"email": "[email protected]",
"password": "qwer#1234"
}`,
wantCode: http.StatusInternalServerError,
wantBody: "系统错误",
},
{
name: "密码必须大于8位,包含数字、特殊字符",
mock: func(ctrl *gomock.Controller) (service.UserService, service.CodeService) {
userSvc := svcmocks.NewMockUserService(ctrl)
codeSvc := svcmocks.NewMockCodeService(ctrl)
return userSvc, codeSvc
},
reqBody: `{
"email": "[email protected]",
"password": "qwer1234"
}`,
wantCode: http.StatusBadRequest,
wantBody: "密码必须大于8位,包含数字、特殊字符",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// 固定写法
ctrl := gomock.NewController(t)
defer ctrl.Finish()

// 启动服务器的流程
server := gin.Default()
userService, codeService := tc.mock(ctrl)
handler := NewUserHandler(userService, codeService)
handler.RegisterRouters(server)

// 构建请求
request, err := http.NewRequest(http.MethodPost, "/users/signup", bytes.NewReader([]byte(tc.reqBody)))
request.Header.Set("Content-Type", "application/json")
assert.NoError(t, err)

// 将请求交给gin来接管,并将结果返回到recorder
recorder := httptest.NewRecorder()
server.ServeHTTP(recorder, request)

// 进行校验
assert.Equal(t, tc.wantCode, recorder.Code)
assert.Equal(t, tc.wantBody, recorder.Body.String())
})
}
}

使用sqlmock来mock数据库(DB)

mock数据库不使用gomock工具生成mock文件

安装依赖

1
go get github.com/DATA-DOG/go-sqlmock

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
func TestUserDao_Insert(t *testing.T) {

testcases := []struct {
name string
mock func(t *testing.T) *sql.DB

user User

wantErr error
}{
{
name: "插入成功",
mock: func(t *testing.T) *sql.DB {
mockDb, mock, err := sqlmock.New()
require.NoError(t, err)

mock.ExpectExec("INSERT INTO .*").
WillReturnResult(sqlmock.NewResult(1, 1))

return mockDb
},
user: User{
Nickname: "haha",
},
wantErr: nil,
},
{
name: "邮箱冲突",
mock: func(t *testing.T) *sql.DB {
mockDb, mock, err := sqlmock.New()
require.NoError(t, err)

mock.ExpectExec("INSERT INTO .*").
WillReturnError(&mysqlDriver.MySQLError{
Number: 1062,
})

return mockDb
},
user: User{
Email: sql.NullString{
String: "[email protected]",
Valid: true,
},
Nickname: "haha",
},
wantErr: ErrDuplicateEmail,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
mockDb := tc.mock(t)
db, err := gorm.Open(mysql.New(mysql.Config{
Conn: mockDb,
SkipInitializeWithVersion: true,
}), &gorm.Config{
DisableAutomaticPing: true,
SkipDefaultTransaction: true})
if err != nil {
return
}
assert.NoError(t, err)

dao := NewUserDao(db)
err = dao.Insert(context.Background(), tc.user)
assert.Equal(t, tc.wantErr, err)
})
}
}