Golang单元测试系列-如何更好的写测试用例

2022-06-16 19:45:38 浏览数 (1)

前面写了快速上手,会非常快速的创建测试用例,搭建一个单元测试的架子,但是如何来更好的写测试用例呢?

我们如何来提升 单测的交互呢 ?

使用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)
		})
	})
}

 说明:

  1. 这里在导入goconvey包的时候通过“.”,省略了调用包内函数时的包名前缀,是goconvey推荐的写法。
  2. convey函数支持平铺罗列,也支持二层、三层嵌套,用于更细粒度拆分单测用例,一般写两层就够了。注意只有外层需要将testing对象t传入进去,内层不需要。
  3. 需要注意外层的代码会随着内层的convey函数执行而执行。
  4. 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的操作,测试就非常方便,我们只需要关注 测试业务代码本身是否符合预期就可以了。

下一次我们再说一下,如何更快速的编写测试用例。

0 人点赞