前面写了快速上手,会非常快速的创建测试用例,搭建一个单元测试的架子,但是如何来更好的写测试用例呢?
我们如何来提升 单测的交互呢 ?
使用goconvey提升单测交互
GoConvey是一个非常好用的Go测试框架,它直接与go test集成,提供了很多丰富的断言函数,能够在终端输出可读的彩色测试结果,还支持全自动的Web UI。
当然我们大部分时间是不使用web UI的,都是流水线集成测试。
下面使用goconvey对从身份证号获取出生日期的函数实现单测
代码语言:javascript复制package util
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestGetBirthdayFromIdNumber(t *testing.T) {
Convey("测试从身份证号获取YYYY-MM-DD格式的出生日期", t, func() {
// 注意这里的代码会被执行多次
// ......
Convey("校验码为X的正常身份证号", func() {
var (
idNumber = "37078620000101023X"
want = "2000-01-01"
)
got := GetBirthdayFromIdNumber(idNumber)
So(got, ShouldEqual, want)
})
Convey("入参非身份证号", func() {
var (
idNumber = "20000101"
)
got := GetBirthdayFromIdNumber(idNumber)
So(got, ShouldBeEmpty)
})
})
}
说明:
- 这里在导入goconvey包的时候通过“.”,省略了调用包内函数时的包名前缀,是goconvey推荐的写法。
- convey函数支持平铺罗列,也支持二层、三层嵌套,用于更细粒度拆分单测用例,一般写两层就够了。注意只有外层需要将testing对象t传入进去,内层不需要。
- 需要注意外层的代码会随着内层的convey函数执行而执行。
- goconvey提供了So断言函数,和很多断言规则,例如ShouldEqual(判断完全相等)、ShouldResemble(相当于reflect.deepEqual,用于map、slice、指针类型的相等判断)、ShouldBeNil(判断等于nil)、ShouldBeLessThan(数字类型大小比较)、ShouldHappenBefore(时间类型判断)等,具体可查阅官方文档。同时,goconvey也支持自定义断言规则,用于自定义判断规则。
执行go test -v -run Xxx,获取单测结果,可以看到测试用例层级展示,每个So断言对应一个√,出错时可以方便的定位到失败的断言。
mock依赖
如果我们依赖数据库或者其他的接口,该如何来mock 呢 ?
我们现在一般都是用gomock来mock代码,使用gomock的前提是,在你实现代码的时候必须很好的去抽象,把所有的数据库代码或者接口代码都抽象成go的接口,之后通过 mockgen 来生成 gomock的代码。就像下面的代码: MysqlService 抽象mysql操作
使用gomock给接口打桩
gomock是Go官方提供的测试框架,它可以对代码中的接口类型进行mock,方便编写单元测试。
如果我们平时遵守依赖倒置原则编写代码,那么使用gomock就会很方便。
需要写单测的业务代码如下,可以看到我们GetStudentTermResult方法中包含了两个mysql查询,按照上文说的单测设计原则,这是需要被mock的行为。
file - student.go
代码语言:javascript复制package school
import (
"GoTesting/util"
)
// StudentService 学生模块service层
type StudentService struct {
m MysqlService
}
// GetStudentTermResult 获取学生学期总结,包括期末成绩、挂科情况
func (s *StudentService) GetStudentTermResult(sId, term string) (*TermResultModel, error) {
// 学生信息
name, err := s.m.GetStudentName(sId)
if err != nil {
return nil, err
}
// 获取学期的时间范围
sTime, eTime := util.GetTermTimeRange(term)
// 查询指定时间段内的学生所有成绩
scores, err := s.m.GetStudentScores(sId, sTime, eTime)
if err != nil {
return nil, err
}
// 统计个人通过率
courseNumber := int32(len(scores)) // 课程数量
passNumber := int32(0) // 及格课程数量
for _, score := range scores {
if score >= 60 {
passNumber
}
}
return &TermResultModel{
Name: name,
Term: term,
Scores: scores,
CourseNumber: courseNumber,
FailedNumber: courseNumber - passNumber,
PassRate: float32(passNumber) / float32(courseNumber),
}, nil
}
file - mysql.go
代码语言:javascript复制package school
// MysqlService 抽象mysql操作
type MysqlService interface {
GetStudentName(sId string) (string, error)
GetStudentScores(sId, sTime, eTime string) (map[string]float32, error)
}
type Mysql struct {
}
// GetStudentName 获取学生姓名
func (m *Mysql) GetStudentName(sId string) (n string, err error) {
// 从mysql存储中select学生表的具体逻辑
// ......
return
}
// GetStudentScores 查询学生指定时间段内的所有考试成绩
func (m *Mysql) GetStudentScores(sId, sTime, eTime string) (scores map[string]float32, err error) {
// 从mysql存储中select成绩表的具体逻辑
// ......
return
}
接下来通过gomock实现单测。
首先使用mockgen命令自动生成mock代码,
代码语言:javascript复制 mockgen -destination file_mock.go -package school -source file.go
下图就是自动生成的mock代码,文件头部会自动带上 DO NOT EDIT. 编辑器也会提升我们不需要编辑,每次调整都使用上面的命令重新生成一遍。
这样就很好的解决外部依赖的问题。
这样在 为我们的业务代码:GetStudentTermResult 编写测试用例的时候,就可以直接mock MySQL的操作,测试就非常方便,我们只需要关注 测试业务代码本身是否符合预期就可以了。
下一次我们再说一下,如何更快速的编写测试用例。