单元测试是最好的文档#
Go默认是带单元测试框架的,只需要新建一个xxx_test.go文件并引入test包即可
1
2
3
4
5
6
7
8
9
10
|
package test
import (
"testing"
"math/rand"
)
func TestRandomFail(t *testing.T) {
if rand.Int31() % 2 == 0 { t.Fail() }
}
|
但实际开发生产环境中,我们的业务代码没有想象中那么简单
举例一个典型场景,用户登录成功后查询积分,无论登陆成功或失败都记录一条日志
这里将动作分解成三个步骤
- 用户登录接口(User.Login)
- 查询积分接口(User.QueryCredit)
- 记录日志接口(StoreLog)
以下是伪代码
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
|
func (u *User) Login() (err error) {
//生成登录Token
u.Token = uuid.New().String()
return nil
}
func (u *User) QueryCredit() (credit uint, err error) {
//从三方接口获取积分
thirdPart := &finace.Object{}
return thirdPart.GetCredit(u.Token)
}
func StoreLog(log string) (err error) {
//写入SQL日志
return gormdb.Exec("INSERT INTO U_Login_Log VALUES (?)", log)
}
func TestQueryCredit(t *testing.T) {
loginErr := user.Login()
credit, creditErr := user.QueryCredit()
storeErr := StoreLog("UserLoginResult:", loginErr, "Credit:", credit, "CreditResult:", creditErr)
if loginErr != nil || creditErr != nil || storeErr != nil {
t.Fatalf("GotError:", loginErr, creditErr, storeErr)
}
}
|
这段代码可能存在的两个问题
- 从三方接口获取积分,但三方接口没有准备好,影响开发进度
- 写入SQL日志依赖数据库,但本地或测试环境没有数据库,流程跑不通
我们要核心解决的问题是伪造接口(Mock)
- 伪造一个成功/失败的三方接口来保证开发进度(MockFunction)
- 伪造一个数据库实例进行增删改查(MockSQL)
Go的Mock库(GoMonkey)#
Monkey的具体介绍不在此展开,其主要用到的功能为
- 打桩某个函数(ApplyFunc)
- 打桩成员方法(ApplyMethod)
- 打桩成员变量(ApplyFuncVar)
已知thirdPart.GetCredit是成员方法,这里需要Mock掉,伪代码为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
func (u *User) QueryCredit() (credit uint, err error) {
//从三方接口获取积分
thirdPart := &finace.Object{}
//第一个参数为成员类型指针
//第二个参数为成员方法名
//第三个参数为成员方法函数(第一个参数固定为成员类型指针,之后在写入参)
patch := monkey.ApplyMethod(
//成员类型指针
reflect.TypeOf(thirdPart),
//成员方法名(字符串)
"GetCredit",
//成员方法函数,第一个参数与成员类型指针对应
func (_ *finace.Object, token string) (uint, error) {
return 66666, nil
}
)
//测试完毕后需要释放Mock
defer patch.Reset()
return thirdPart.GetCredit(u.Token)
}
|
这段代码最后执行的结果就是为finace对象的GetCredit打桩
从代码上看结果永远返回66666, nil
Go的SQLMock库(go-sqlmock)#
在数据库应用开发过程中,会在数据库上执行各种SQL语句
但在做单测时,一般不会与实际数据库交互,这时就需要Mock
即在不建立真实连接的情况下,模拟sql driver中的各种操作
go-sqlmock只会生成sql.db对象,如果使用了gorm则需特殊处理如
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
//db是sql.db对象指针
//mock是需要打桩的函数列表
func Mock() {
db, mock, _ := sqlmock.New()
ormdb, err := gorm.Open(
sql.New(
sql.Config{
//传入SQL.DB连接池(Mock)
Conn: db,
//跳过获取SQL版本,不开会报错
SkipInitializeWithVersion: true
}
)
)
//打桩INSERT函数
mock.ExpectExec("INSERT").WillReturnResult(sqlmock.NewResult(0, 0))
//打桩QUERY函数
mock.ExpectQuery("SHOW").WillReturnRows(sqlmock.NewRows([]string{""}))
}
|
最终Mock的代码为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
func StoreLog(log string) (err error) {
db, mock, _ := sqlmock.New()
ormdb, err := gorm.Open(sql.New(
sql.Config{
Conn: db,
SkipInitializeWithVersion: true
}
)
)
//Mock掉INSERT语句,注意区分大小写
//WillReturnResult是与其返回的结果,如果是SELECT语句需要Mock数据结构
//具体不在展开
mock.ExpectExec("INSERT").WillReturnResult(sqlmock.NewResult(0, 0))
return gormdb.Exec("INSERT INTO U_Login_Log VALUES (?)", log)
}
|
参考文章#